<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~files/atom-premium.xsl"?>
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedpress="https://feed.press/xmlns" xmlns:media="http://search.yahoo.com/mrss/" xmlns:podcast="https://podcastindex.org/namespace/1.0">
  <feedpress:locale>en</feedpress:locale>
  <feedpress:newsletterId>telerik-blogs-web</feedpress:newsletterId>
  <link rel="hub" href="https://feedpress.superfeedr.com/"/>
  <logo>https://static.feedpress.com/logo/telerik-blogs--web-5ab52bef0a72f.jpg</logo>
  <title type="text">Telerik Blogs | Web</title>
  <subtitle type="text">The official blog of Progress Telerik - expert articles and tutorials for developers.</subtitle>
  <id>uuid:904671b7-469e-496f-bdd8-197c8325d207;id=2612</id>
  <updated>2026-06-06T22:09:25Z</updated>
  <link rel="alternate" href="https://www.telerik.com/"/>
  <link rel="self" type="application/atom+xml" href="https://feeds.telerik.com/blogs/web"/>
  <entry>
    <id>urn:uuid:288b4cc6-8c92-4dec-a932-bed600474298</id>
    <title type="text">Angular 22: The Evolution of Modern Angular</title>
    <summary type="text">Signals, Zoneless, declarative async resources: Angular 22 makes standard several features that help developers build better apps.</summary>
    <published>2026-06-05T21:01:27Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Dany Paredes </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17354975/angular-22-evolution-modern-angular"/>
    <content type="text"><![CDATA[<p><span class="featured">Signals, Zoneless, declarative async resources: Angular 22 makes standard several features that help developers build better apps.</span></p><p>A few days ago, we checked out <a target="_blank" href="https://www.telerik.com/blogs/google-io-2026-developers-moving-ai-agentic-era">Google I/O 2026</a>, and today we need to talk about our favorite framework: <strong>Angular</strong>. Why? Because <strong>version 22</strong> is here!</p><p>Angular v22 is a major update focused on stability, performance and ergonomic APIs. It brings a more polished signal-driven platform and better data loading primitives that make modern apps feel faster and simpler to build.</p><p>Before we look at the most interesting changes, let&rsquo;s talk about the foundation. Angular v22 stabilizes the features we have been testing in the last few versions. If you want to build a high-performance app today, these are the new standards.</p><h2 id="stable-signal-forms-and-httpresource">Stable Signal Forms and httpResource</h2><p>Reactive Forms were great, but they often felt separate from the modern Signal-based reactivity. In v22, <strong>Signal Forms are officially stable</strong>.</p><p>What does this mean for us?</p><ul><li><strong>Better reactivity:</strong> No more unnecessary updates when a single field changes.</li><li><strong>Better type safety:</strong> We finally have a form system that works perfectly with TypeScript.</li></ul><p>Do you remember <code>HttpClient</code> and the manual <code>toSignal</code> conversions? They worked, but they added extra code. Now we have <code>httpResource</code>.</p><p>It is the new standard for fetching data. It treats your HTTP requests as signals, automatically handling the loading, error, and data states for you.</p><blockquote><p><strong>Learn more:</strong> Dive deeper into <a target="_blank" href="https://www.telerik.com/blogs/getting-started-httpresource-api-angular">Getting Started with httpResource API</a> and learn more about <a target="_blank" href="https://www.telerik.com/blogs/angular-signal-forms-vs-reactive-forms">Angular Signal Forms vs. Reactive Forms</a>.</p></blockquote><p>But what if your request depends on another signal? In v22, we have the new <code>chain()</code> method. It allows you to link resources together. If the first resource is loading, the second one waits automatically. This is a big win for complex data!</p><p><strong>Wait, what if you are already using RxJS?</strong> Don&rsquo;t worry, Angular has a solution for that too. Let&rsquo;s see how v22 handles the transition from Observables to Resources.</p><h2 id="rxresource-vs.-httpresource">RxResource vs. httpResource</h2><p>A common question is: &ldquo;Should I stop using RxJS?&rdquo; The answer is <strong>no</strong>. Angular v22 introduces <code>rxResource</code> as the perfect partner for your existing Observable streams.</p><ul><li><strong><code>httpResource</code></strong>: Use this for standard API calls. It is optimized for the Fetch API and handles JSON automatically.</li><li><strong><code>rxResource</code></strong>: Use this when your data source is an existing Observable. It allows you to use the power of RxJS operators inside the clean, Signal-based Resource API.</li></ul><p>Now that we have our data fetching ready, how do we make sure our users and agents can navigate efficiently? Let&rsquo;s talk about the new smart navigation features in the Router.</p><h2 id="smart-navigation-and-incremental-hydration">Smart Navigation and Incremental Hydration</h2><p>The Angular Router received some updates in v22 that solve common problems.</p><ul><li><strong>URL decoupling:</strong> You can now keep a clean URL in the address bar while the app shows a different state. With the new <code>browserUrl</code> input on <code>RouterLink</code>, you can show <code>/profile</code> while the router actually uses <code>/users/123</code>.</li><li><strong>Parameter inheritance:</strong> Child routes now inherit parameters from their parents by default, and you don&rsquo;t need extra configuration to get an ID from a parent route.</li></ul><p>But a great UI and smart routing mean nothing if the page is slow to load. This brings us to the most impressive performance update in v22: <strong>incremental hydration</strong>.</p><p>Server-Side Rendering (SSR) is no longer a problem. In v22, <strong>incremental hydration is the default</strong>.</p><p>Angular now loads your application in small parts as they become visible. Combined with <strong>resource caching</strong>, the client can use the data fetched on the server without making a second API call. The user sees the data instantly, and the &ldquo;flicker&rdquo; of reloading data is gone.</p><blockquote><p><strong>Learn more:</strong> Explore <a target="_blank" href="https://www.telerik.com/blogs/new-angular-hydration">The New Angular Hydration</a> for in-depth details.</p></blockquote><p>But performance is also about how the browser handles changes. This is where the biggest change in Angular&rsquo;s history reaches its peak.</p><h2 id="zoneless-and-onpush-by-default">Zoneless and OnPush by Default</h2><p>For years, <code>zone.js</code> was the engine behind Angular&rsquo;s change detection. With v22, <strong>Angular is now Zoneless by default</strong>.</p><p>By using <code>ChangeDetectionStrategy.OnPush</code> as the default for new components, we get:</p><ul><li><strong>Smaller app sizes:</strong> We no longer need the heavy Zone.js library.</li><li><strong>Fast performance:</strong> The framework only checks what it needs to, exactly when it needs to.</li></ul><p>If you have an older app, don&rsquo;t worry! There is a migration tool that adds <code>ChangeDetectionStrategy.Eager</code> (the new name for the old behavior) so your app keeps working while you move to Signals.</p><p>Now, how do we make sure our fast code actually works? Let&rsquo;s talk about the new speed king of testing.</p><h2 id="vitest">Vitest</h2><p>Testing used to be the slow part of our work. Not anymore. Angular v22 officially replaces Karma/Jasmine with <strong>Vitest</strong> as the default test runner.</p><p>It is very fast and shares the same configuration as your build pipeline. Also, the new <code>migrate-karma-to-vitest</code> tool makes the change very easy, even for complex tests!</p><blockquote><p>Do you want to try <a target="_blank" href="https://www.telerik.com/blogs/unit-testing-angular-modern-testing-vitest">Vitest</a>?</p></blockquote><h2 id="angular-aria-accessibility-for-everyone">Angular Aria: Accessibility for Everyone</h2><p>Building custom components with correct ARIA roles and focus management can be difficult. In v22, <strong>Angular Aria is officially stable</strong>.</p><p>Think of it as <strong>&ldquo;headless accessibility.&rdquo;</strong> It provides the logic you need to build your own accessible components. It handles the complicated parts of ARIA attributes and keyboard interactions for you.</p><p>Why does this matter for AI? Because accessible apps are much easier for AI Agents to understand!</p><blockquote><p>Check this example about <a target="_blank" href="https://www.telerik.com/blogs/build-accessible-components-angular-aria">how to build accessible components with Angular Aria</a>.</p></blockquote><h2 id="developer-experience--safety">Developer Experience &amp; Safety</h2><p>In v22, we finally have <strong><code>@boundary</code></strong> as part of the preview story. It is useful for future error-handling patterns. Here&rsquo;s how <code>@boundary</code> works for error containment:</p><pre class=" language-typescript"><code class="prism  language-typescript">@<span class="token function">Component</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
 selector<span class="token punctuation">:</span> <span class="token string">'app-user-profile'</span><span class="token punctuation">,</span>
 standalone<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
 template<span class="token punctuation">:</span> <span class="token template-string"><span class="token string">`
   &lt;div&gt;
     &lt;h2&gt;User Profile&lt;/h2&gt;
     &lt;p&gt;{{ userData() }}&lt;/p&gt;
   &lt;/div&gt;
 `</span></span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UserProfileComponent</span> <span class="token punctuation">{</span>
 userData <span class="token operator">=</span> <span class="token function">signal</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


@<span class="token function">Component</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
 selector<span class="token punctuation">:</span> <span class="token string">'app-dashboard'</span><span class="token punctuation">,</span>
 standalone<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
 imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>UserProfileComponent<span class="token punctuation">]</span><span class="token punctuation">,</span>
 template<span class="token punctuation">:</span> <span class="token template-string"><span class="token string">`
   &lt;div&gt;
     &lt;h1&gt;Dashboard&lt;/h1&gt;
     @boundary on error {
       &lt;p&gt;Failed to load user profile&lt;/p&gt;
     } {
       &lt;app-user-profile /&gt;
     }
   &lt;/div&gt;
 `</span></span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">DashboardComponent</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h3 id="selectorless-components">Selectorless Components</h3><p>You no longer need a selector for a component that only lives in a route. It reduces extra code.</p><pre class=" language-typescript"><code class="prism  language-typescript">@<span class="token function">Component</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  standalone<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
  <span class="token comment">// No selector needed! Perfect for routes or dynamic loading</span>
  template<span class="token punctuation">:</span> <span class="token template-string"><span class="token string">`&lt;h3&gt;I'm a lean, selectorless component!&lt;/h3&gt;`</span></span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">DetailViewComponent</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h3 id="async-dependency-injection">Async Dependency Injection</h3><p>The <code>injectAsync()</code> function allows us to load services only when they are needed. This keeps your app small and fast.</p><pre class=" language-typescript"><code class="prism  language-typescript"><span class="token keyword">async</span> <span class="token function">onReportRequested</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// We only load the heavy AnalyticsService when the user clicks!</span>
  <span class="token keyword">const</span> analytics <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">injectAsync</span><span class="token punctuation">(</span>AnalyticsService<span class="token punctuation">)</span><span class="token punctuation">;</span>
  analytics<span class="token punctuation">.</span><span class="token function">track</span><span class="token punctuation">(</span><span class="token string">'report_viewed'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h2 id="building-a-dashboard-with-angular-22">Building a Dashboard with Angular 22</h2><p>Instead of just explaining features, let&rsquo;s build a practical dashboard example that uses the stable core of Angular v22: <code>@Service()</code>, <code>httpResource()</code> and the new template syntax.</p><blockquote><p><strong>Want to follow along?</strong> You can clone the complete project from GitHub: <a target="_blank" href="https://github.com/danywalls/angular-22-nba-finals">angular-22-nba-finals</a>. Just run <code>npm start</code> and you&rsquo;ll have the dashboard running locally.</p></blockquote><h3 id="the-data-service-with-service">The Data Service with @Service</h3><p>Now, let&rsquo;s create our service. We will use the new <code>@Service()</code> decorator (which replaces <code>@Injectable()</code>) and <code>httpResource</code> to fetch the games. Notice how <code>httpResource</code> handles loading and error states for you.</p><pre class=" language-typescript"><code class="prism  language-typescript"><span class="token comment">// src/app/nba.service.ts</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Service <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/core'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> httpResource <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/common/http'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">NbaGame</span> <span class="token punctuation">{</span>
  id<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
  gameNumber<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
  homeTeam<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  homeScore<span class="token punctuation">:</span> <span class="token keyword">number</span> <span class="token operator">|</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
  awayTeam<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  awayScore<span class="token punctuation">:</span> <span class="token keyword">number</span> <span class="token operator">|</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
  status<span class="token punctuation">:</span> <span class="token string">'Final'</span> <span class="token operator">|</span> <span class="token string">'Scheduled'</span><span class="token punctuation">;</span>
  date<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  location<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

@<span class="token function">Service</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">NbaService</span> <span class="token punctuation">{</span>
  <span class="token comment">// httpResource is the standard for fetching data</span>
  gamesResource <span class="token operator">=</span> httpResource<span class="token operator">&lt;</span>NbaGame<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token string">'/api/nba-finals'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="the-ui-layer--error-states">The UI Layer &amp; Error States</h3><p>We display the data using the new <code>@for</code> block and keep the error handling inside the component template. This keeps the example focused on stable Angular v22 behavior.</p><pre class=" language-typescript"><code class="prism  language-typescript"><span class="token comment">// src/app/app.ts</span>
@<span class="token function">Component</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  template<span class="token punctuation">:</span> <span class="token template-string"><span class="token string">`
    &lt;app-dashboard&gt;&lt;/app-dashboard&gt;
    &lt;router-outlet&gt;&lt;/router-outlet&gt;
  `</span></span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
</code></pre><p>Our Dashboard component remains simple:</p><pre class=" language-typescript"><code class="prism  language-typescript"><span class="token comment">// src/app/dashboard.component.ts</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Component<span class="token punctuation">,</span> inject <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/core'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> NbaService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./nba.service'</span><span class="token punctuation">;</span>

@<span class="token function">Component</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  selector<span class="token punctuation">:</span> <span class="token string">'app-dashboard'</span><span class="token punctuation">,</span>
  standalone<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
  template<span class="token punctuation">:</span> <span class="token template-string"><span class="token string">`
    &lt;div class="dashboard"&gt;
      &lt;h1&gt;NBA Finals 2026: Knicks vs Spurs&lt;/h1&gt;

      @if (nbaService.gamesResource.isLoading()) {
         &lt;p&gt;Loading game data...&lt;/p&gt;
      } @else {
        &lt;div class="cards"&gt;
          @for (game of nbaService.gamesResource.value(); track game.id) {
            &lt;div class="card"&gt;
              &lt;div class="meta"&gt;Game {{ game.gameNumber }} &bull; {{ game.date }}&lt;/div&gt;
              &lt;div class="score"&gt;
                {{ game.homeTeam }} {{ game.homeScore }} vs {{ game.awayScore }} {{ game.awayTeam }}
              &lt;/div&gt;
              &lt;div class="status"&gt;{{ game.status }}&lt;/div&gt;
            &lt;/div&gt;
          }
        &lt;/div&gt;
      }
    &lt;/div&gt;
  `</span></span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">DashboardComponent</span> <span class="token punctuation">{</span>
  nbaService <span class="token operator">=</span> <span class="token function">inject</span><span class="token punctuation">(</span>NbaService<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>Save changes and run <code>ng server</code> navigate in the browser and tada!!!</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-06/angular-22-signals-dashboard.png?sfvrsn=5aeda1c4_2" alt="Angular 22 example dashboard using signals and httpResource" /></p><p>Done!! We play with the stable part of Angular v22 using <code>@Service()</code> for a clean singleton service definition, <code>httpResource()</code> for declarative, signal-based HTTP loading and <code>@for</code> with <code>@if</code> in templates to render loading and data states.</p><p>And, of course, AI is not forgotten by the Angular team. They updated and added new tools for Angular MCP and Skills. Let&rsquo;s see them!</p><h2 id="angular-mcp-and-skills">Angular MCP and Skills</h2><p>Of course, Angular continues embracing AI. The Angular MCP exposes app state and builds workflows for tools that understand Angular projects. That includes server-side MCP tools such as <code>devserver.wait_for_build</code>, <code>devserver.start</code> and <code>devserver.stop</code>.</p><p>Version 22 also introduces Agent Skills for Angular, a lightweight set of skill definitions that give AI assistants direct knowledge of modern Angular idioms and best practices. These skills are useful for teams that want to integrate agent-assisted coding into their workflow without pain.</p><ul><li>Learn more about Angular MCP: <a target="_blank" href="https://angular.dev/ai/mcp">angular.dev/ai/mcp</a></li><li>Learn more about Agent Skills for Angular: <a target="_blank" href="https://github.com/angular/skills">github.com/angular/skills</a></li></ul><h2 id="recap">Recap</h2><p>Angular 22 is a declaration that the way we build for the web has evolved. And by using <strong>Signals</strong>, <strong>Zoneless performance</strong> and <strong>declarative async resources</strong>, we can build applications that are faster, simpler and easier to maintain.</p><p>This release is about making stable patterns:</p><ul><li>Use <code>httpResource()</code> for fetch-heavy components.</li><li>Use <code>@Service()</code> for singleton services.</li><li>Use <code>injectAsync()</code> when you want to defer heavy service loading.</li><li>Let template syntax like <code>@for</code> and <code>@if</code> keep your view code readable.</li><li>Embrace the AI era with Agent Skills and MCP tools.</li></ul><p>My advice is don&rsquo;t just read this article&mdash;build something! Take the dashboard we sketched and turn it into an interactive experience with Progress <a target="_blank" href="https://www.telerik.com/kendo-angular-ui/ai-chat">Kendo UI for Angular AI Chat</a>. A chat interface is a powerful way to explore the series score in real time and it&rsquo;s a great way to test Angular 22 with a modern Kendo UI scenario.</p><p>Feel free to <a target="_blank" href="https://youtu.be/h5OJUSS_8IA?is=WBc_bZuuQnx9b0EY">watch for all the new features in Angular 22</a>:</p><div data-sf-ec-immutable="" class="-sf-relative" contenteditable="false" style="width:560px;height:315px;"><div data-sf-disable-link-event=""><iframe width="560" height="315" src="https://www.youtube.com/embed/h5OJUSS_8IA?si=0Nzrxxm5BbAjRJz6" title="What’s new in Angular v22" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe></div></div><p><strong>Happy coding!</strong></p><img src="https://feeds.telerik.com/link/10827/17354975.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:f5808969-92f6-4e38-b801-f36a47627abf</id>
    <title type="text">Visual Studio 2026, 6 Months Later</title>
    <summary type="text">Throughout this post, we’ll discuss Visual Studio updates and improvements, such as performance, GitHub Copilot, Hot Reload and more.</summary>
    <published>2026-06-04T21:42:24Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Dave Brock </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17354313/visual-studio-2026-6-months-later"/>
    <content type="text"><![CDATA[<p><span class="featured">Throughout this post, we&rsquo;ll discuss Visual Studio updates and improvements, such as performance, GitHub Copilot, Hot Reload and more.</span></p><p>I remember exactly where I was when <a target="_blank" href="https://devblogs.microsoft.com/visualstudio/visual-studio-2026-is-here-faster-smarter-and-a-hit-with-early-adopters/">Visual Studio 2026 dropped</a> back in November: at my desk, watching the Microsoft launch event with one eye and refactoring an <code>IDataReader</code> implementation with the other.</p><p>The keynote was typical Microsoft. There was a lot of &ldquo;reimagining how developers work.&rdquo; There was, of course, a slide with the word &ldquo;intelligent&rdquo; on it at least eight times. And the word &ldquo;Copilot&rdquo; appeared on screen so many times I started to wonder if Microsoft&rsquo;s marketing team had been replaced by an infinite loop that just prints <code>Copilot</code> forever.</p><p>Then I installed the thing.</p><p>The first week with VS 2026 was the most disoriented I&rsquo;ve felt in a Visual Studio release since <a target="_blank" href="https://devblogs.microsoft.com/visualstudio/a-design-with-all-caps/">the menu bar went all-caps in 2012</a> and I spent a month reading angry forum threads about it.</p><p>In 2026, I was tripped up by the just-slightly different Solution Explorer, the Copilot panel that lives where my Test Explorer used to live, the general Copilot onslaught and the interesting design choices. I felt like everything on my desk had been moved slightly to the left.</p><p>That was temporary. As with most new technology, eventually things felt normal again. I was finding workflows that genuinely beat my old ones and I stopped noticing the cosmetic changes. So here&rsquo;s where I am six months later &hellip; same machine, same kind of solution I work in every day, just a different IDE around it.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-06/vs2026-overview.png?sfvrsn=4ef376fb_2" alt="" /><br /><span style="font-size:14px;">We meet again, old friend. At least the menu bar isn't in caps.</span></p><p>Six months in, I feel comfortable. Six months is long enough to live with the rough edges, and find what&rsquo;s genuinely useful. So I&rsquo;m here to share them with you.</p><p>As an aside: to keep my sanity intact, I&rsquo;m going to use the word &ldquo;Copilot&rdquo; only when I absolutely have to. Microsoft has decided that every feature in Visual Studio is now Copilot-something &hellip; Copilot Chat, Copilot Edits, Copilot Agent, Copilot Workspaces, GitHub Copilot, Copilot for Azure, Copilot for Pull Requests and so on. So when I say &ldquo;the agent&rdquo; or &ldquo;the AI thing in the sidebar,&rdquo; you&rsquo;ll know what I mean.</p><h2 id="the-performance-story-is-real">The Performance Story Is Real</h2><p>I&rsquo;ll lead with this, mostly because it was the thing I was most skeptical about. Every Visual Studio release for the past decade has touted &ldquo;X% faster solution load!&rdquo; and, to me, every Visual Studio release for the last decade has felt (roughly) the same speed once you test it with your beefy codebase.</p><p>VS 2026 is genuinely faster. Not &ldquo;marketing-faster&rdquo; &hellip; actually faster. The <a target="_blank" href="https://devblogs.microsoft.com/visualstudio/visual-studio-2022/">64-bit-everywhere story that started with VS 2022</a> has matured into something that handles large solutions without the periodic &ldquo;please hold&rdquo; pauses we&rsquo;re all familiar with. In Microsoft&rsquo;s VS2026 launch post, they say they <a target="_blank" href="https://devblogs.microsoft.com/visualstudio/visual-studio-2026-is-here-faster-smarter-and-a-hit-with-early-adopters/">cut hangs by over 50%</a>.</p><p>The IntelliSense responsiveness is the part I notice most. Typing a <code>.</code> after an object now produces suggestions roughly when I expect them to appear. A low bar? Definitely, but previous versions frequently limboed under it.</p><h2 id="agent-mode-is-useful-when-you-pick-the-right-job">Agent Mode Is Useful, When You Pick the Right Job</h2><p>GitHub Copilot&rsquo;s <a target="_blank" href="https://learn.microsoft.com/en-us/visualstudio/ide/copilot-agent-mode?view=visualstudio">agent mode in Visual Studio 2026</a>&mdash;or, as Microsoft prefers, <em>GitHub Copilot Agent Mode powered by Copilot in Visual Studio with Copilot</em>&mdash;has improved tremendously since Visual Studio 2022. Six months in, I think it&rsquo;s genuinely useful, but for a specific set of tasks.</p><p>In my experience, it works well with scoped, well-defined refactors. If I want to ask: <em>Hey Copilot, add a new health check for this Cosmos container and follow the pattern in my existing health checks.</em> (Yes, I sometimes talk to Copilot like a home assistant.) Or: <em>Take my new controller and add the same logging pattern as the others in this project.</em> And, after discovering some methods in a codebase with 11 nested if statements: <em>Convert this <code>for</code> loop to a LINQ expression but keep it readable.</em> It&rsquo;s effective enough that I&rsquo;ve stopped writing this kind of code by hand.</p><p>In my experience, things get harder when the agent needs to understand why something exists. The agent will happily refactor a piece of code that only exists because of, say, a weird third-party integration constraint from 2018, and the resulting code will be cleaner, more maintainable &hellip; and broken in production. Did I actually break something in production? Not this time. But I would have.</p><p>To be fair to the agent, this is exactly where it needs context. It doesn&rsquo;t read minds (although it sure does try). Here&rsquo;s a pattern I&rsquo;ve leaned on.</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token comment">// AGENT NOTES:</span>
<span class="token comment">// - This service must remain synchronous; the upstream caller cannot</span>
<span class="token comment">//   be made async without a contract change.</span>
<span class="token comment">// - The string "PRIME" is required by the third-party integration. </span>
<span class="token comment">//   I don't like it but they won't change it. Do not "improve" it </span>
<span class="token comment">//   to PersonType.Prime.ToString().</span>
<span class="token comment">// - Cancellation tokens must be forwarded; do NOT pass `default`.</span>
</code></pre><p>When the agent has clear context like this, it really delivers. Below is the agent producing two new health check files plus a registration update in <code>Program.cs</code>, all from one prompt asking it to mirror an existing pattern. The result is a careful refactoring with the right substitutions swapped in. That&rsquo;s exactly the kind of work I&rsquo;m happy to hand off.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-06/gh-copilot-agent.png?sfvrsn=a2b5eefe_2" alt="" /><br /><span style="font-size:14px;">The agent panel and one of the two generated files. Three files changed, zero hand-written by me. (My team is thankful for the latter.)</span></p><p>This sounds like babysitting, and it is. Welcome to AI-driven development, my friends. The 10 seconds of setup saves me from a 10-minute cleanup later. When the prep is solid, the result is solid.</p><p>For more information on <a target="_blank" href="https://learn.microsoft.com/en-us/visualstudio/ide/copilot-chat-context?view=visualstudio#use-custom-instructions">custom instructions, check out the Microsoft documentation</a>.</p><h2 id="test-explorer-isnt-frustrating">Test Explorer Isn&rsquo;t Frustrating</h2><p>In my experience, the Test Explorer seems to be much improved in Visual Studio 2026. If I had to sum it up in previous versions, I&rsquo;d say &ldquo;it&rsquo;s fine&rdquo; &hellip; after all, do we ever get jazzed about tests? In previous releases, I kept saying &ldquo;it&rsquo;s fine&rdquo; until I had a few thousand tests, at which point it started getting opinionated about which tests to run.</p><p>The new version is faster, the filtering predictably works, and it doesn&rsquo;t lose track of which tests are running when you switch branches mid-execution. Try it yourself. Run your full test suite and watch the Test Explorer not embarrass itself. It&rsquo;s a small joy.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-06/test-explorer.png?sfvrsn=222a8ba_2" alt="" /><br /><span style="font-size:14px;">One of those things you don't notice until it stops getting in your way.</span></p><h2 id="better-git-integration">Better Git Integration</h2><p>Visual Studio&rsquo;s Git tooling has historically been one of the IDE&rsquo;s more underwhelming features. Until recently, my workflow was:</p><ol><li>Make code changes in Visual Studio.</li><li>Run Git commands in my terminal (or a third-party tool like GitHub Desktop)</li><li>Ignore the IDE&rsquo;s Git panel entirely.</li></ol><p>With VS 2026, I&rsquo;m finally doing real Git work inside the tool. The new staging interface is better. The conflict resolution UI is also better than it used to be. The &ldquo;please show me an actual diff and not a vague summary&rdquo; feature is good.</p><p>Now, it&rsquo;s no GitKraken and it doesn&rsquo;t try to be. But it&rsquo;s good enough that I no longer switch over to a terminal for routine operations, and that works for me.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-06/git-conflict.png?sfvrsn=62f8884a_2" alt="" /><br /><span style="font-size:14px;">Three-way merge view, in the IDE I'm already in. Rejoice!</span></p><h2 id="hot-reload-that-reloads-more-often">Hot Reload That Reloads More Often</h2><p>Hot Reload has been one of those features that worked on Friday afternoons if I said &ldquo;please work&rdquo; (especially for ASP.NET Core). Any other time, I&rsquo;d change a method body, save, watch the IDE think very hard about it, and then get: <em>&ldquo;Hot Reload was unable to apply your changes. Restart the application?&rdquo;</em></p><p>The &ldquo;hot&rdquo; in &ldquo;Hot Reload&rdquo; was <a target="_blank" href="https://www.reddit.com/r/dotnet/comments/1my662s/will_microsoft_ever_fix_hot_reload_in_net/">stretching the truth</a> at times (with the understanding that getting it right <a target="_blank" href="https://www.reddit.com/r/dotnet/comments/1my662s/comment/naac88s/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button">isn&rsquo;t as simple in .NET</a> as it is for languages like JavaScript).</p><p>With VS 2026, I now reach for it instinctively instead of cynically. Method-body edits in controllers, services and Razor pages just apply. You change a string in a controller, hit Save and see the change. You know, the way it was supposed to work in 2021. The way it kind of worked in 2023. The way it now actually works in 2026.</p><p>When Hot Reload can&rsquo;t apply a change (because the change requires a metadata update or you&rsquo;ve added a field or whatever the actual reason is), the IDE now tells me clearly why instead of vaguely throwing up its hands. That alone goes a long way. I know whether to keep iterating or to just restart the host.</p><h3 id="debugger-improvements-especially-with-async-traces">Debugger Improvements (Especially with Async Traces)</h3><p>Have you ever stared at an async stack trace and felt your soul leave your body? Just me?</p><p>Historically, when an exception bubbled up from somewhere deep in an <code>async</code>/<code>await</code> chain, the stack trace was a wall of <code>MoveNext</code> calls and state machine internals. It was tremendously useful if you wanted to learn about the C# compiler, but not great if you wanted to know which of your methods actually threw.</p><p>Things are looking better in Visual Studio 2026. The debugger now stitches together async call chains into something that reads like the call graph you actually wrote. The <code>MoveNext</code> noise is collapsed by default. The original calling method is shown as the parent frame and not buried six levels down. When an exception is thrown inside a <code>Task.WhenAll</code>, you can see which task threw without reverse-engineering the parallel structure.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-06/debugger.png?sfvrsn=53980e30_2" alt="" /><br /><span style="font-size:14px;">An async exception, with the actual call chain visible. No <code>MoveNext</code> translation required.</span></p><p>On top of that, conditional breakpoints feel faster to evaluate, and the tracepoints (where you can log a message without breaking) is a one-click affair instead of a buried right-click option.</p><p>Small wins. But debugging is the thing I do every day, and small wins on the thing you do every day add up fast.</p><h2 id="the-verdict">The Verdict</h2><p>Look, the Copilot stuff is genuinely impressive. The agent demos at the keynote were the kind of thing that makes you want to fire up the installer immediately. And after six months, I&rsquo;ll admit that I use it every day.</p><p>But Visual Studio 2026 is a genuine step forward for the boring reasons. The performance is real. The Test Explorer doesn&rsquo;t stress me out. The debugger provides stack traces a human can read. The agent mode handles the kind of work I&rsquo;d previously do by hand. Those are the wins that show up in my daily work, not the ones that show up in keynote demos.</p><p>If you&rsquo;re on Visual Studio 2022 and wondering whether to upgrade: yes. Ignore the marketing, and let the unglamorous improvements do their job. Six months later, you won&rsquo;t be excited about Visual Studio 2026 &hellip; and that&rsquo;s exactly the point. You&rsquo;ll just be working faster.</p><aside><hr data-sf-ec-immutable="" /><div class="row"><div class="col-4 u-normal-full u-small-mb0"><h4 class="u-fs20 u-fw5 u-lh125 u-mb0">Extension Properties: C# 14&rsquo;s Game-Changing Feature for Cleaner Code</h4></div><div class="col-8"><p class="u-fs16 u-mb0">This post introduces <a target="_blank" href="https://www.telerik.com/blogs/extension-properties-csharp-14-game-changing-feature-cleaner-code">extension properties</a>, a new feature of C# 14 that allows you to use properties in your extension methods.</p></div></div></aside><img src="https://feeds.telerik.com/link/10827/17354313.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:3187b492-d5dc-4bc2-94f1-80f26e3730cb</id>
    <title type="text">Google I/O 2026 for Developers: Moving from AI to the Agentic Era</title>
    <summary type="text">ICYMI: Google I/O 2026 announced how the tech giant is embracing the Agentic Era. Here’s what it means for developers.</summary>
    <published>2026-06-03T20:16:05Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Dany Paredes </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17353557/google-io-2026-developers-moving-ai-agentic-era"/>
    <content type="text"><![CDATA[<p><span class="featured">ICYMI: Google I/O 2026 announced how the tech giant is embracing the Agentic Era. Here&rsquo;s what it means for developers.</span></p><p>Another year, another Google I/O. This time, the vibe is different. We are not just talking about &ldquo;AI in a sidebar&rdquo; or &ldquo;chatbots.&rdquo; We are witnessing the birth of the Agentic Era.</p><p><a target="_blank" href="https://www.youtube.com/watch?v=FZ-3BjbfNlI">Watch the Google I/O Developer Keynote Recap</a>:</p><div data-sf-ec-immutable="" class="-sf-relative" contenteditable="false" style="width:560px;height:315px;"><div data-sf-disable-link-event=""><iframe width="560" height="315" src="https://www.youtube.com/embed/FZ-3BjbfNlI?si=f7jP61PYQ6wE0qp1" title="Developer Keynote (Google I/O '26)" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe></div></div><br /><p>As developers, we have all felt the pain: building a beautiful UI, only to have an AI agent try to &ldquo;scrape&rdquo; it. The agent fails to find the right button or misinterprets a CSS class. In 2026, Google is changing the game by providing the infrastructure for agents that do not just &ldquo;chat,&rdquo; but actually act.</p><p>Let&rsquo;s dive into the key updates from the <a target="_blank" href="https://developers.googleblog.com/all-the-news-from-the-google-io-2026-developer-keynote/">Developer Keynote</a> that will change how we build software this year.</p><p>These are the top highlights of Google I/O 2026 for developers:</p><ul><li>Gemini 3.5 and Managed Agents: Act, Don&rsquo;t Just Talk.</li><li>Antigravity 2.0 and Managed Agents: The Agent-First Infrastructure.</li><li>WebMCP: Making Your UI &ldquo;Agent-Ready.&rdquo;</li><li>AI-Powered DevTools: Debugging for Humans and Agents.</li><li>Android: The End of Slow Migrations.</li><li>Firebase, Built-in AI and Performance: The Core of the Agentic Web.</li></ul><p>Let&rsquo;s explore each one.</p><h2 id="gemini-3.5-and-managed-agents-act-dont-just-talk">1. Gemini 3.5 and Managed Agents: Act, Don&rsquo;t Just Talk</h2><p>The biggest shift this year is the introduction of Gemini 3.5 and the Managed Agents API. This is not just about a smarter LLM: it is about orchestration.</p><p>With a single API call, you can now set up a remote sandbox and an agent harness. This allows you to deploy autonomous agents that can execute code, manage files and interact with your backend. You do not have to manage the underlying compute or security infrastructure.</p><p>This integration goes beyond just code generation: it includes a &ldquo;one-click deploy&rdquo; from Google AI Studio directly to Cloud Run. This makes the path from prototype to production much faster. For Android developers, native Kotlin support enables a &ldquo;vibe coding&rdquo; experience. In this mode, the model understands complex app structures as if it were a senior teammate.</p><blockquote><ul><li><a target="_blank" href="https://aistudio.google.com/">Learn more about Gemini 3.5 capabilities.</a></li><li><a target="_blank" href="https://youtu.be/aqmpZocmR8o?t=149">Watch Gemini 3.5 in action.</a></li></ul></blockquote><p>But having a brain capable of action is only the first step. To truly use this power, we need an ecosystem that can safely manage these agents. This is where the next generation of infrastructure comes in.</p><h2 id="antigravity-2.0-and-managed-agents-the-agent-first-infrastructure">2. Antigravity 2.0 and Managed Agents: The Agent-First Infrastructure</h2><p>The most powerful announcement for our productivity is the launch of Antigravity 2.0 and the all-new Antigravity CLI. This platform is more than just a tool: it is a complete surface for managing specialized subagents. These subagents can tackle complex workflows, such as UI development or backend migrations. The best part is that all of this happens while being protected by built-in terminal sandboxing and credential masking.</p><p>If you want to skip the infrastructure setup, the new Managed Agents in the Gemini API is a great solution. With one API call, Google provides a fully set-up agent harness with a remote sandbox. This removes the friction of setup, allowing you to focus on the logic of your agent.</p><blockquote><ul><li><a target="_blank" href="https://developers.google.com/antigravity">Check out the Antigravity SDK documentation.</a></li><li><a target="_blank" href="https://www.youtube.com/watch?v=aqmpZocmR8o&amp;t=2717s">Watch Antigravity 2.0 in action.</a></li></ul></blockquote><p>Once we have the infrastructure to build and deploy agents, the next challenge is enabling our web applications to actually talk back to them. Let&rsquo;s look at how Google is standardizing this conversation.</p><h2 id="webmcp-making-your-ui-‘agent-ready’">3. WebMCP: Making Your UI &lsquo;Agent-Ready&rsquo;</h2><p>On the web side, the big news is WebMCP (Model Context Protocol for the Web). This protocol allows your website to expose structured tools directly to browser-based AI agents.</p><p>If you have been following my work on the Progress <a target="_blank" href="https://www.telerik.com/blogs/angular-kendo-ui-mcp-making-agents-work-for-you">Kendo UI for Angular MCP Server</a>, you know how powerful this is. While Kendo UI MCP helps the developer build apps by giving the agent specific knowledge, WebMCP helps the user&rsquo;s agent navigate the app once it is built. Together, they create a complete AI lifecycle from creation to consumption.</p><p>Instead of an AI agent &ldquo;guessing&rdquo; how to interact with your data by clicking randomly, it can discover and use specific tools via WebMCP. For example, it can use a &ldquo;Filter&rdquo; or &ldquo;Export&rdquo; command in a Kendo UI Grid. This improves reliability and security for the end user.</p><blockquote><ul><li><a target="_blank" href="https://www.youtube.com/watch?v=bo3i0FzDUYo">Discover WebMCP Architecture.</a></li></ul></blockquote><p>With our apps now acting as toolkits for agents, we need a way to see what is happening under the hood. Thankfully, the debugging tools we use every day have also received an AI-first update.</p><h2 id="ai-powered-devtools-debugging-for-humans-and-agents">4. AI-Powered DevTools: Debugging for Humans and Agents</h2><p>One of the most innovative updates is the transformation of Chrome DevTools into an AI-first environment. Google has introduced the AI Assistance panel. This is a context-aware chat interface powered by Gemini that understands the technical state of your page.</p><p>To enable these features today, make sure your DevTools language is set to English (US). Then, enable the &ldquo;AI Innovations&rdquo; flag in the DevTools Settings.</p><p>The dedicated DevTools interface for AI agents also grants autonomous assistants access to the accessibility tree and network traffic. This allows AI coding agents to identify and fix bugs or performance issues by themselves.</p><blockquote><ul><li><a target="_blank" href="https://www.youtube.com/watch?v=aqmpZocmR8o&amp;t=2717s">Watch AI Assistance in DevTools.</a></li></ul></blockquote><p>But the shift toward an agentic future is not limited to the browser. These same patterns are now landing in the mobile ecosystem, starting with a massive update to how we migrate and build Android apps.</p><h2 id="android-the-end-of-slow-migrations">5. Android: The End of Slow Migrations</h2><p>For mobile developers, the Android Migration Agent in Android Studio is a breakthrough. It can migrate source code from React Native or iOS into native Kotlin Android apps very quickly.</p><p>This is not just about translating syntax: the agent understands the architectural details of Navigation 3 and modern Jetpack libraries. By using open-source Android Skills, the migration follows industry-standard patterns. This means you do not just get a working app, you get a high-quality native codebase that follows Google&rsquo;s best practices.</p><blockquote><ul><li><a target="_blank" href="https://github.com/android/skills">Explore Android Skills on GitHub.</a></li><li><a target="_blank" href="https://www.youtube.com/watch?v=dXCCleAddEA">Watch Android AI Updates.</a></li></ul></blockquote><p>None of these platforms would matter without a high-performance engine. To close our recap, let&rsquo;s look at the core browser APIs and Firebase updates that make this entire experience possible.</p><h2 id="firebase-built-in-ai-and-performance">6. Firebase, Built-in AI and Performance</h2><p>Firebase is also becoming agent-ready with Firebase Data Connect. This feature brings PostgreSQL to Firebase, allowing you to build full-stack apps with AI Studio integration. You can now build and launch apps directly to Cloud Run without leaving your development environment.</p><p>On the browser side, Chrome 148 brings the Prompt API (powered by Gemini Nano) to the stable channel. It is now multimodal, which means you can process images, audio and text directly on the user&rsquo;s device. The inclusion of Gemma 197M means these models are fast, even on older hardware.</p><p>Additionally, the new Soft Navigations API finally treats client-side transitions in SPAs as first-class citizens. This gives us accurate Core Web Vitals for our Angular dashboards. We also saw the release of HTML-in-Canvas, which allows us to render real DOM elements inside 3D environments.</p><blockquote><ul><li><a target="_blank" href="https://developer.chrome.com/blog/chrome-148-beta/">Check the Chrome 148 release notes.</a></li><li><a target="_blank" href="https://www.youtube.com/watch?v=uT7MVcCQ4rw">Watch Performance Updates.</a></li></ul></blockquote><h2 id="recap-why-these-tools-matter">Recap: Why These Tools Matter</h2><p>Google I/O 2026 has made it clear: the browser and the OS are no longer just renderers. They are execution environments for intelligence. The shift from AI Assistance to Agentic Workflows is a massive opportunity for us as developers to redefine how users interact with software.</p><p>By making the web machine-readable with WebMCP and providing the Antigravity platform, Google is setting the stage for a proactive web. Our role is shifting from building static interfaces to building intelligent platforms.</p><h3 id="what-you-can-do-now">What You Can Do Now</h3><ol><li><strong>Experiment with WebMCP:</strong> Think about which repetitive task in your current app could be handled by a secure subagent or a structured tool. You can also play with the <a target="_blank" href="https://www.telerik.com/blogs/angular-kendo-ui-mcp-making-agents-work-for-you">Kendo UI MCP</a> to see it in action!</li><li><strong>Optimize for Soft Navigations:</strong> If you are building web apps with React or Angular, start using the new API to get the real performance story of your dashboards.</li><li><strong>Try the Managed Agents API:</strong> Test the loop between AI Studio and Cloud Run to see how fast you can go from idea to an agentic prototype.</li></ol><p>If you have time, you can also <a target="_blank" href="https://io.google/2026/explore/developer-keynote-1">check out the full video of the Developer Keynote</a> for even more demos and deep dives.</p><p>Happy coding!</p><hr data-sf-ec-immutable="" /><blockquote><aside><div class="row"><div class="col-4 u-normal-full u-small-mb0"><h4 class="u-fs20 u-fw5 u-lh125 u-mb0">Telerik and Kendo UI Meet WebMCP</h4></div><div class="col-8"><p class="u-fs16 u-mb0">Learn how Progress Telerik UI and Kendo UI tools <a target="_blank" href="https://www.telerik.com/blogs/telerik-and-kendo-meet-webmcp"> leverage WebMCP</a> to expose enterprise application functionality directly to AI agents through standardized tools.</p></div></div></aside></blockquote><img src="https://feeds.telerik.com/link/10827/17353557.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:54ada40d-ff98-496d-94ae-1dbc6613c6e0</id>
    <title type="text">Advanced Form Validation in Blazor 10</title>
    <summary type="text">These new validation features available in Blazor as of .NET 10 help manage forms with nested objects and lists, plus perform complex and cross-field validations.</summary>
    <published>2026-06-03T13:34:16Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Héctor Pérez </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17353276/advanced-form-validation-blazor-10"/>
    <content type="text"><![CDATA[<p><span class="featured">These new validation features available in Blazor as of .NET 10 help manage forms with nested objects and lists, plus perform complex and cross-field validations.</span></p><p>Form validation is essential when working with data in Blazor applications. Before .NET 10, if you wanted to perform validation of nested objects, you had to take a series of additional steps that could add complexity to your projects. In this article, we will examine the improvements and new features in .NET 10 to solve this problem through a practical exercise. Let&rsquo;s go!</p><h2 id="understanding-the-problem-of-nested-validations-before-.net-10">Understanding the Problem of Nested Validations Before .NET 10</h2><p>To see the problem when working with nested objects and their validation, let&rsquo;s create a sample Blazor project. For this demonstration, I am working with a project using the <strong>Blazor Web App</strong> template configured with <strong>Interactive Server</strong> rendering <strong>globally</strong>.</p><p>Immediately after creating the project, perform the <a target="_blank" href="https://www.telerik.com/blazor-ui/documentation/getting-started/web-app">installation and configuration steps for the Progress Telerik UI for Blazor controls</a>, as we will use them to quickly build modern interfaces.</p><h3 id="creating-models-in-the-application">Creating Models in the Application</h3><p>Suppose we are building an attendee registration application for a technical event. As a first step, I have defined a class <code>EventRegistration</code> with the following structure:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">EventRegistration</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> Attendee Attendee <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> BillingAddress BillingAddress <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> List<span class="token operator">&lt;</span>SessionSelection<span class="token operator">&gt;</span> Sessions <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>

    <span class="token punctuation">[</span><span class="token function">Range</span><span class="token punctuation">(</span><span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token keyword">bool</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">"true"</span><span class="token punctuation">,</span> <span class="token string">"true"</span><span class="token punctuation">,</span> ErrorMessage <span class="token operator">=</span> <span class="token string">"You must accept the terms and conditions."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">bool</span> AcceptTerms <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The model above has some nested objects, both single objects such as <code>Attendee</code> and a list of type <code>SessionSelection</code>. Likewise, there is a validation on the property <code>AcceptTerms</code> of type <code>Range</code>, which verifies that the value is <code>true</code>.</p><p>The model of a <code>Attendee</code> looks as follows:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Attendee</span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Full name is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token punctuation">[</span><span class="token function">StringLength</span><span class="token punctuation">(</span><span class="token number">100</span><span class="token punctuation">,</span> ErrorMessage <span class="token operator">=</span> <span class="token string">"Name cannot exceed 100 characters."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> FullName <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Email is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token punctuation">[</span><span class="token function">EmailAddress</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Invalid email format."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> Email <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Phone number is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token punctuation">[</span><span class="token function">Phone</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Invalid phone number format."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> Phone <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Company name is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> Company <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Job title is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> JobTitle <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The <code>Attendee</code> model has more validation annotations per property, which allow validating each attendee&rsquo;s information. The project also includes the <code>BillingAddress</code> model to store address information, also featuring validation attributes:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">BillingAddress</span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Street address is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> Street <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"City is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> City <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"State/Province is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> State <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Postal code is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> PostalCode <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Country is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> Country <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Finally, the <code>SessionSelection</code> model defines the structure of each session selected by an attendee:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SessionSelection</span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Session name is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> SessionName <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token function">Required</span><span class="token punctuation">(</span>ErrorMessage <span class="token operator">=</span> <span class="token string">"Preference level is required."</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span><span class="token operator">?</span> Preference <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The complexity among the previous models will allow us to test the new validation features included in .NET 10.</p><h3 id="initializing-project-data-using-a-session-catalog-service">Initializing Project Data Using a Session Catalog Service</h3><p>To display mock data in the application, let&rsquo;s create a <code>SessionCatalog</code> service, where we&rsquo;ll define a list of sessions:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SessionCatalog</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> List<span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span> <span class="token function">GetAvailableSessions</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span>
    <span class="token punctuation">[</span>
        <span class="token string">"Keynote: The Future of .NET"</span><span class="token punctuation">,</span>
        <span class="token string">"Workshop: Blazor Advanced Patterns"</span><span class="token punctuation">,</span>
        <span class="token string">"Talk: SignalR Real-Time Communication"</span><span class="token punctuation">,</span>
        <span class="token string">"Workshop: Minimal APIs Deep Dive"</span><span class="token punctuation">,</span>
        <span class="token string">"Talk: Cloud-Native .NET Applications"</span><span class="token punctuation">,</span>
        <span class="token string">"Workshop: AI Integration with .NET"</span>
    <span class="token punctuation">]</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> List<span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span> <span class="token function">GetPreferenceLevels</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span>
    <span class="token punctuation">[</span>
        <span class="token string">"High"</span><span class="token punctuation">,</span>
        <span class="token string">"Medium"</span><span class="token punctuation">,</span>
        <span class="token string">"Low"</span>
    <span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>We will register this service in <code>Program.cs</code> as follows:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">var</span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method function">AddSingleton<span class="token punctuation">&lt;</span>SessionCatalog<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">var</span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><h3 id="creating-the-graphical-interface">Creating the Graphical Interface</h3><p>To perform various checks, let&rsquo;s create a graphical interface using the <code>TelerikForm</code> component. This will enable generating and customizing model-based forms and with many configuration parameters, making it a powerful and useful component for our example. The resulting code looks like this:</p><pre class=" language-xml"><code class="prism  language-xml">@page "/registration"
@rendermode InteractiveServer
@using FormValidationNET10.Models
@using FormValidationNET10.Services
@inject SessionCatalog SessionCatalog

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PageTitle</span><span class="token punctuation">&gt;</span></span>Event Registration<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PageTitle</span><span class="token punctuation">&gt;</span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mb-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Event Registration<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">&gt;</span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikForm</span> <span class="token attr-name">EditContext</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>editContext<span class="token punctuation">"</span></span>
             <span class="token attr-name">OnValidSubmit</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>HandleValidSubmit<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormValidation</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DataAnnotationsValidator</span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikValidationSummary</span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>FormValidation</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItems</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Attendee.FullName<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Full Name<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Attendee.Email<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Email<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Attendee.Phone<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Phone<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Attendee.Company<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Company<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Attendee.JobTitle<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Job Title<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>BillingAddress.Street<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Street<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>BillingAddress.City<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>City<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>BillingAddress.State<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>State / Province<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>BillingAddress.PostalCode<span class="token punctuation">"</span></span> <span class="token attr-name">LabelText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Postal Code<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>BillingAddress.Country<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Template</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>country-editor<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>k-label k-form-label<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Country<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>k-form-field-wrap<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikDropDownList</span> <span class="token attr-name">@bind-Value</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>registration.BillingAddress.Country<span class="token punctuation">"</span></span>
                                         <span class="token attr-name">Data</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@countries<span class="token punctuation">"</span></span>
                                         <span class="token attr-name">DefaultText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Select a country...<span class="token punctuation">"</span></span>
                                         <span class="token attr-name">Id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>country-editor<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikValidationMessage</span> <span class="token attr-name">For</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@(() =&gt; registration.BillingAddress.Country)<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Template</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>FormItem</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItem</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>AcceptTerms<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Template</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>k-form-field-wrap<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikCheckBox</span> <span class="token attr-name">@bind-Value</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>registration.AcceptTerms<span class="token punctuation">"</span></span> <span class="token attr-name">Id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>acceptTerms<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>acceptTerms<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>ms-2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>I accept the terms and conditions<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span>
                        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikValidationMessage</span> <span class="token attr-name">For</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@(() =&gt; registration.AcceptTerms)<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Template</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>FormItem</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>FormItems</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormItemsTemplate</span> <span class="token attr-name">Context</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>formContext<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
        @{
            var items = formContext.Items.OfType<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IFormItem</span><span class="token punctuation">&gt;</span></span>().ToList();
        }

        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card shadow-sm mb-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-header bg-primary text-white<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Attendee Information<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-body<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>row g-3<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-6<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "Attendee.FullName"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-6<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "Attendee.Email"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "Attendee.Phone"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "Attendee.Company"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "Attendee.JobTitle"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>

        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card shadow-sm mb-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-header bg-primary text-white<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Billing Address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-body<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>row g-3<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-12<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "BillingAddress.Street"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "BillingAddress.City"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "BillingAddress.State"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "BillingAddress.PostalCode"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-12<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "BillingAddress.Country"))" /&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>

        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card shadow-sm mb-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-header bg-primary text-white d-flex justify-content-between align-items-center<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Session Selection<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikButton</span> <span class="token attr-name">OnClick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@AddSession<span class="token punctuation">"</span></span>
                               <span class="token attr-name">ThemeColor</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@ThemeConstants.Button.ThemeColor.Light<span class="token punctuation">"</span></span>
                               <span class="token attr-name">ButtonType</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@ButtonType.Button<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                    + Add Session
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TelerikButton</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-body<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                @if (registration.Sessions.Count == 0)
                {
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>alert alert-info mb-0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                        No sessions added yet. Click "Add Session" to select the sessions you'd like to attend.
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                }
                else
                {
                    @for (int i = 0; i &lt; registration.Sessions.Count; i++)
                    {
                        var index = i;
                        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>row g-3 mb-3 align-items-end<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-5<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>k-label k-form-label<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Session<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span>
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikDropDownList</span> <span class="token attr-name">@bind-Value</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>registration.Sessions[index].SessionName<span class="token punctuation">"</span></span>
                                                     <span class="token attr-name">Data</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@availableSessions<span class="token punctuation">"</span></span>
                                                     <span class="token attr-name">DefaultText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Select a session...<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikValidationMessage</span> <span class="token attr-name">For</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@(() =&gt; registration.Sessions[index].SessionName)<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>k-label k-form-label<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Preference<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span>
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikDropDownList</span> <span class="token attr-name">@bind-Value</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>registration.Sessions[index].Preference<span class="token punctuation">"</span></span>
                                                     <span class="token attr-name">Data</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@preferenceLevels<span class="token punctuation">"</span></span>
                                                     <span class="token attr-name">DefaultText</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Select preference...<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikValidationMessage</span> <span class="token attr-name">For</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@(() =&gt; registration.Sessions[index].Preference)<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>col-md-3<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikButton</span> <span class="token attr-name">OnClick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@(() =&gt; RemoveSession(index))<span class="token punctuation">"</span></span>
                                               <span class="token attr-name">ThemeColor</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@ThemeConstants.Button.ThemeColor.Error<span class="token punctuation">"</span></span>
                                               <span class="token attr-name">ButtonType</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@ButtonType.Button<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                                    Remove
                                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TelerikButton</span><span class="token punctuation">&gt;</span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
                    }
                }
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikValidationMessage</span> <span class="token attr-name">For</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@(() =&gt; registration.Sessions)<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>

        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card shadow-sm mb-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-body<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                &lt;TelerikFormItemRenderer Item="@(items.First(x =&gt; x.Field == "AcceptTerms"))" /&gt;
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>FormItemsTemplate</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>FormButtons</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikButton</span> <span class="token attr-name">ButtonType</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@ButtonType.Submit<span class="token punctuation">"</span></span>
                       <span class="token attr-name">ThemeColor</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@ThemeConstants.Button.ThemeColor.Primary<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            Submit Registration
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TelerikButton</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>FormButtons</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TelerikForm</span><span class="token punctuation">&gt;</span></span>

@if (isSubmitted)
{
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>alert alert-success mt-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span><span class="token punctuation">&gt;</span></span>Registration Successful!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">&gt;</span></span>Thank you, <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>strong</span><span class="token punctuation">&gt;</span></span>@registration.Attendee.FullName<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>strong</span><span class="token punctuation">&gt;</span></span>! Your registration for @registration.Sessions.Count session(s) has been submitted.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
}

@code {
    private EventRegistration registration = new();
    private EditContext editContext = default!;
    private List<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>string</span><span class="token punctuation">&gt;</span></span> availableSessions = [];
    private List<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>string</span><span class="token punctuation">&gt;</span></span> preferenceLevels = [];
    private List<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>string</span><span class="token punctuation">&gt;</span></span> countries = ["United States", "Canada", "Mexico", "United Kingdom", "Germany", "France", "Spain"];
    private bool isSubmitted;

    protected override void OnInitialized()
    {
        editContext = new EditContext(registration);
        
        availableSessions = SessionCatalog.GetAvailableSessions();
        preferenceLevels = SessionCatalog.GetPreferenceLevels();
    }

    private void AddSession()
    {
        registration.Sessions.Add(new SessionSelection());
    }

    private void RemoveSession(int index)
    {
        registration.Sessions.RemoveAt(index);
    }

    private void HandleValidSubmit(EditContext context)
    {
        isSubmitted = true;
    }
}
</code></pre><h3 id="validating-data-before-.net-10">Validating Data Before .NET 10</h3><p>Once we have all the pieces of the project, let&rsquo;s run it. Let&rsquo;s do a test by trying to submit the empty form:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/form-post-nested-objects-binding-before-net10.gif?sfvrsn=8b4e6fd5_2" alt="Form post showing nested object binding failures before .NET 10" /></p><p>In the previous image, you can see that when submitting the form the only error shown is the one indicating that the terms and conditions have not been accepted. However, although all models implement validations, no others are shown.</p><p>The reason behind this is that <code>DataAnnotationsValidator</code> in Blazor does not perform validation on nested objects or collections. This has been a known limitation in Blazor since its beginnings, which was addressed using custom recursive validators. Let&rsquo;s see how .NET 10 helps us solve this problem.</p><h2 id="validating-data-thanks-to-the-new-features-in-.net-10">Validating Data Thanks to the New Features in .NET 10</h2><p>To perform validation of nested objects and collections, the first step you must take is to add in <code>Program.cs</code> a call to the method <code>AddValidation()</code>:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">var</span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddValidation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">var</span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>The method <code>AddValidation()</code> registers the services required for the validation system to work.</p><p>The next step is to open the main model that contains the hierarchy of submodels and add the attribute <code>[ValidatableType]</code> to the class. In our example, the class is <code>EventRegistration</code>, which will look as follows after the modification:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>ValidatableType<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">EventRegistration</span>
<span class="token punctuation">{</span>
   <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token punctuation">}</span>
</code></pre><p>The attribute <code>[ValidatableType]</code> tells the .NET source generator to generate validation logic for the class. Behind the scenes the following steps will be performed:</p><ol><li>All properties of the decorated class are inspected.</li><li>Nested objects are detected and the necessary code to recursively validate their validation annotations is generated.</li><li>Collections are detected and code is generated to iterate through each element, validating their validation annotations.</li></ol><p>After applying the previous changes, you will see that now, when submitting the form with empty fields, all errors are displayed correctly:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/form-validating-nested-objects-net10.gif?sfvrsn=60d57217_2" alt="Web form displaying nested object fields with validation" /></p><p>With these changes, we have correctly enabled full validation of nested objects and collections.</p><h3 id="implementing-cross-field-validation-using-ivalidatableobject">Implementing Cross-field Validation Using IValidatableObject</h3><p>So far we have been able to apply validation rules at the level of individual properties. However, it is common in complex applications to have rules that involve multiple properties or complex business logic. For these cases, .NET provides the interface <code>IValidatableObject</code>, which in version 10 integrates seamlessly with <code>[ValidatableType]</code>.</p><p>Let&rsquo;s perform a check of this type assuming this pair of business rules:</p><ul><li>The user must select at least one session.</li><li>Duplicate sessions are not allowed.</li></ul><p>The rules depend on the state of the collection <code>Sessions</code> as a whole. To make it work correctly, let&rsquo;s modify the class <code>EventRegistration</code> by adding the interface:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>ValidatableType<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">EventRegistration</span> <span class="token punctuation">:</span> IValidatableObject
<span class="token punctuation">{</span>
    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>

    <span class="token keyword">public</span> IEnumerable<span class="token operator">&lt;</span>ValidationResult<span class="token operator">&gt;</span> <span class="token function">Validate</span><span class="token punctuation">(</span>ValidationContext validationContext<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
    <span class="token comment">// 1.</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>Sessions<span class="token punctuation">.</span>Count <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">yield</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">ValidationResult</span><span class="token punctuation">(</span>
                <span class="token string">"You must select at least one session."</span><span class="token punctuation">,</span>
                <span class="token punctuation">[</span><span class="token function">nameof</span><span class="token punctuation">(</span>Sessions<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        
        <span class="token comment">// 2.</span>
        <span class="token keyword">var</span> duplicates <span class="token operator">=</span> Sessions
            <span class="token punctuation">.</span><span class="token function">Where</span><span class="token punctuation">(</span>s <span class="token operator">=</span><span class="token operator">&gt;</span> <span class="token operator">!</span><span class="token keyword">string</span><span class="token punctuation">.</span><span class="token function">IsNullOrEmpty</span><span class="token punctuation">(</span>s<span class="token punctuation">.</span>SessionName<span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">GroupBy</span><span class="token punctuation">(</span>s <span class="token operator">=</span><span class="token operator">&gt;</span> s<span class="token punctuation">.</span>SessionName<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Where</span><span class="token punctuation">(</span>g <span class="token operator">=</span><span class="token operator">&gt;</span> g<span class="token punctuation">.</span><span class="token function">Count</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&gt;</span> <span class="token number">1</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Select</span><span class="token punctuation">(</span>g <span class="token operator">=</span><span class="token operator">&gt;</span> g<span class="token punctuation">.</span>Key<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">ToList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">if</span> <span class="token punctuation">(</span>duplicates<span class="token punctuation">.</span>Count <span class="token operator">&gt;</span> <span class="token number">0</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">yield</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">ValidationResult</span><span class="token punctuation">(</span>
                $<span class="token string">"Duplicate sessions are not allowed: {string.Join("</span><span class="token punctuation">,</span> <span class="token string">", duplicates)}"</span><span class="token punctuation">,</span>
                <span class="token punctuation">[</span><span class="token function">nameof</span><span class="token punctuation">(</span>Sessions<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>In the code above:</p><ol><li>We validate that at least one session is selected.</li><li>We look for duplicate sessions.</li></ol><p>In case of any error, we return an <code>ValidationResult</code> indicating the error. When running the application, we see the following result if no session is selected:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/validation-no-session-selected.png?sfvrsn=8e250e63_2" alt="Validation error shown for no session selected" /></p><p>Now, when duplicate sessions are selected, the following error is shown:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/validation-warning-duplicate-sessions.png?sfvrsn=38a76043_2" alt="Validation warning indicating duplicate sessions selected" /></p><p>With this, we have created cross-field validation correctly using Blazor.</p><h3 id="improving-validations-using-fieldcssclassprovider">Improving Validations Using FieldCssClassProvider</h3><p>At this point we have a project that correctly validates the form information, thanks to the native capabilities of <code>TelerikForm</code>. We can go one step further and modify the project to add visual indicators at the section level that communicate to the user which sections have errors. We can do this thanks to the method <code>EditContext.IsValid(fieldIdentifier)</code> introduced in .NET 10, which allows directly checking whether a field is valid.</p><p>To use it, the first thing we will do is create a class that inherits from <code>FieldCssClassProvider</code>. For the demonstration I will create a class called <code>RegistrationFieldClassProvider</code> at the root level of the project that looks as follows:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">RegistrationFieldClassProvider</span> <span class="token punctuation">:</span> FieldCssClassProvider
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">override</span> <span class="token keyword">string</span> <span class="token function">GetFieldCssClass</span><span class="token punctuation">(</span>EditContext editContext<span class="token punctuation">,</span> <span class="token keyword">in</span> FieldIdentifier fieldIdentifier<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>editContext<span class="token punctuation">.</span><span class="token function">IsModified</span><span class="token punctuation">(</span>fieldIdentifier<span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token keyword">return</span> <span class="token keyword">string</span><span class="token punctuation">.</span>Empty<span class="token punctuation">;</span>

        <span class="token keyword">var</span> isValid <span class="token operator">=</span> editContext<span class="token punctuation">.</span><span class="token function">IsValid</span><span class="token punctuation">(</span>fieldIdentifier<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> isValid <span class="token operator">?</span> <span class="token string">"is-valid"</span> <span class="token punctuation">:</span> <span class="token string">"is-invalid"</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>In the code above, it checks whether the field has been modified. Next <code>IsValid()</code> is used inside the method <code>GetFieldCssClass</code> to check if the field has associated validation messages. Finally, Bootstrap classes are returned that will provide feedback to the user about the validity of the sections.</p><p>To achieve this, we will make the card headers change color according to the state of their fields by modifying <code>Registration.razor</code> as follows:</p><ol><li>Add the event <code>OnValidSubmit</code> to <code>TelerikForm</code>:</li></ol><pre class=" language-xml"><code class="prism  language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikForm</span> <span class="token attr-name">EditContext</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>editContext<span class="token punctuation">"</span></span>
             <span class="token attr-name">OnValidSubmit</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>HandleValidSubmit<span class="token punctuation">"</span></span>
             <span class="token attr-name">OnInvalidSubmit</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>HandleInvalidSubmit<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
</code></pre><ol start="2"><li>Find the <code>h5</code> elements that reference <strong>Attendee Information</strong> and <strong>Billing Address</strong>, and modify their container <code>div</code> as follows:</li></ol><pre class=" language-xml"><code class="prism  language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-header @GetAttendeeHeaderClass()<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Attendee Information<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
...
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-header @GetBillingHeaderClass()<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Billing Address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
</code></pre><ol start="3"><li>Modify the code section by adding the following changes:</li></ol><pre class=" language-csharp"><code class="prism  language-csharp">@code <span class="token punctuation">{</span>
   <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
    <span class="token keyword">private</span> <span class="token keyword">bool</span> hasValidated<span class="token punctuation">;</span>

    <span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token keyword">void</span> <span class="token function">OnInitialized</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        editContext <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">EditContext</span><span class="token punctuation">(</span>registration<span class="token punctuation">)</span><span class="token punctuation">;</span>
                
        editContext<span class="token punctuation">.</span><span class="token function">SetFieldCssClassProvider</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">RegistrationFieldClassProvider</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        editContext<span class="token punctuation">.</span>OnValidationStateChanged <span class="token operator">+</span><span class="token operator">=</span> <span class="token punctuation">(</span>_<span class="token punctuation">,</span> _<span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> <span class="token function">StateHasChanged</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        

        availableSessions <span class="token operator">=</span> SessionCatalog<span class="token punctuation">.</span><span class="token function">GetAvailableSessions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        preferenceLevels <span class="token operator">=</span> SessionCatalog<span class="token punctuation">.</span><span class="token function">GetPreferenceLevels</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    
    <span class="token keyword">private</span> <span class="token keyword">bool</span> <span class="token generic-method function">IsFieldValid<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>Expression<span class="token operator">&lt;</span>Func<span class="token operator">&lt;</span>T<span class="token operator">&gt;</span><span class="token operator">&gt;</span> accessor<span class="token punctuation">)</span>
       <span class="token operator">=</span><span class="token operator">&gt;</span> editContext<span class="token punctuation">.</span><span class="token function">IsValid</span><span class="token punctuation">(</span>FieldIdentifier<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span>accessor<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">private</span> <span class="token keyword">string</span> <span class="token function">GetAttendeeHeaderClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>hasValidated<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token string">"bg-primary text-white"</span><span class="token punctuation">;</span>

        <span class="token keyword">bool</span> allValid <span class="token operator">=</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>Attendee<span class="token punctuation">.</span>FullName<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>Attendee<span class="token punctuation">.</span>Email<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>Attendee<span class="token punctuation">.</span>Phone<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>Attendee<span class="token punctuation">.</span>Company<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>Attendee<span class="token punctuation">.</span>JobTitle<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">return</span> allValid <span class="token operator">?</span> <span class="token string">"bg-success text-white"</span> <span class="token punctuation">:</span> <span class="token string">"bg-danger text-white"</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">private</span> <span class="token keyword">string</span> <span class="token function">GetBillingHeaderClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>hasValidated<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token string">"bg-primary text-white"</span><span class="token punctuation">;</span>

        <span class="token keyword">bool</span> allValid <span class="token operator">=</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>BillingAddress<span class="token punctuation">.</span>Street<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>BillingAddress<span class="token punctuation">.</span>City<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>BillingAddress<span class="token punctuation">.</span>State<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>BillingAddress<span class="token punctuation">.</span>PostalCode<span class="token punctuation">)</span>
            <span class="token operator">&amp;&amp;</span> <span class="token function">IsFieldValid</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> registration<span class="token punctuation">.</span>BillingAddress<span class="token punctuation">.</span>Country<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">return</span> allValid <span class="token operator">?</span> <span class="token string">"bg-success text-white"</span> <span class="token punctuation">:</span> <span class="token string">"bg-danger text-white"</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>

    <span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">HandleValidSubmit</span><span class="token punctuation">(</span>EditContext context<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>        
        hasValidated <span class="token operator">=</span> <span class="token keyword">true</span><span class="token punctuation">;</span>        
        isSubmitted <span class="token operator">=</span> <span class="token keyword">true</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
        
    <span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">HandleInvalidSubmit</span><span class="token punctuation">(</span>EditContext context<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        hasValidated <span class="token operator">=</span> <span class="token keyword">true</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>In the code above, I added the field <code>hasValidated</code> to check if the form has been submitted. I also modified <code>OnInitialized</code> by adding the handler for the event <code>OnValidationStateChanged</code>, which allows the UI to update reactively. I also added the helper <code>IsFieldValid&lt;T&gt;</code> and the methods <code>GetAttendeeHeaderClass</code> and <code>GetBillingHeaderClass</code>. This is the result:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/form-section-error-summary.png?sfvrsn=64b57388_2" alt="Form displaying an error summary for entire sections" /></p><p>With this, we have managed to give the user better feedback about the sections they need to complete before proceeding.</p><h2 id="conclusion">Conclusion</h2><p>Throughout this article, we have explored the new features available in .NET 10 that will undoubtedly help you manage forms with nested objects and lists. We&rsquo;ve also looked at how to perform cross-validations and, finally, the process of carrying out more complex validations by checking data in sections.</p><p>Now it&rsquo;s your time to implement these new features in your own projects and provide better feedback to your users.</p><aside><hr data-sf-ec-immutable="" /><div class="row"><div class="col-4 u-normal-full u-small-mb0"><h4 class="u-fs20 u-fw5 u-lh125 u-mb0">120+ Enterprise-Grade Blazor UI Components</h4></div><div class="col-8"><p class="u-fs16 u-mb0">Accelerate app development with <a target="_blank" href="https://www.telerik.com https://www.telerik.com/blazor-ui">Telerik UI for Blazor</a>&mdash;a complete set of 120+ high-performance, accessible controls, MCP Servers and more. Available with a free 30-day trial.</p></div></div></aside><img src="https://feeds.telerik.com/link/10827/17353276.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:d3c5b41c-9e8d-4bba-88a3-60fa450dd551</id>
    <title type="text">Create and Publish an NPM Package Automatically</title>
    <summary type="text">Follow these steps to publish a TypeScript pakcage to npm.</summary>
    <published>2026-06-02T21:18:01Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Jonathan Gamble </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17352916/create-publish-npm-package-automatically"/>
    <content type="text"><![CDATA[<p><span class="featured">Follow these steps to publish a TypeScript pakcage to npm.</span></p><p>There are not many good articles on publishing a TypeScript package to npm automatically. The best article I have seen is by the one and only Matt Pocock, <a target="_blank" href="https://www.totaltypescript.com/how-to-create-an-npm-package#32-set-up-a-tsconfigjson">How to Create an NPM Package</a>. This is wonderful for understanding the package part, but publishing is not well documented. Recently, the npm publish keys have gone away, so we must connect our workflow manually with <a target="_blank" href="https://docs.npmjs.com/trusted-publishers">Trusted Publishing</a>.</p><p><strong>Let&rsquo;s go through all steps to publish a TS npm package automagically!</strong></p><h2 id="create-repository">Create Repository</h2><ol><li><p>Create a folder with the name of your package and open it up in VS Code.</p></li><li><p>Create your repository on GitHub with same name.</p></li><li><p>Run <code>git init</code> in root.</p></li><li><p>Create a <code>.gitignore</code> file in root. <code>dist</code> will be where it builds the JavaScript.</p><pre class=" language-bash"><code class="prism  language-bash">node_modules
dist
.env
.env.*
</code></pre></li></ol><p><strong>Note:</strong> You may not have an <code>.env</code> file, but it is good practice.</p><ol start="5"><li>Create your <code>README.md</code> with whatever content.</li><li>Create a <code>LICENSE</code> file if you want as well. There are <a target="_blank" href="https://generate-license-file.js.org/">packages</a> for this or you can copy and paste.</li><li>Run <code>git add .</code> and <code>git commit -m 'first'</code> to commit your first file.</li><li>Run <code>git branch -M main</code> to make <code>main</code> the default branch.</li><li>Add your remote origin with:</li></ol><pre class=" language-bash"><code class="prism  language-bash"><span class="token function">git</span> remote add origin https://github.com/YOUR_REPOSITORY
</code></pre><ol start="10"><li>Push package with <code>git push -u origin main</code>.</li></ol><p><strong>Note:</strong> My example package will be named <code>format-price</code>.</p><h2 id="create-your-package.json-file">Create Your Package.json File</h2><ol><li>Create <code>package.json</code> in root.</li></ol><pre class=" language-bash"><code class="prism  language-bash"><span class="token punctuation">{</span>
  <span class="token string">"name"</span><span class="token keyword">:</span> <span class="token string">"format-price"</span>,
  <span class="token string">"version"</span><span class="token keyword">:</span> <span class="token string">"1.0.0"</span>,
  <span class="token string">"description"</span><span class="token keyword">:</span> <span class="token string">"A demo of a simple TypeScript package that formats a price value."</span>,
  <span class="token string">"keywords"</span><span class="token keyword">:</span> <span class="token punctuation">[</span>
    <span class="token string">"demo"</span>,
    <span class="token string">"typescript"</span>,
    <span class="token string">"price"</span>,
    <span class="token string">"formatting"</span>
  <span class="token punctuation">]</span>,
  <span class="token string">"homepage"</span><span class="token keyword">:</span> <span class="token string">"https://github.com/jdgamble555/format-price"</span>,
  <span class="token string">"bugs"</span><span class="token keyword">:</span> <span class="token punctuation">{</span>
    <span class="token string">"url"</span><span class="token keyword">:</span> <span class="token string">"https://github.com/jdgamble555/format-price/issues"</span>
  <span class="token punctuation">}</span>,
  <span class="token string">"author"</span><span class="token keyword">:</span> <span class="token string">"Jonathan Gamble (https://code.build)"</span>,
  <span class="token string">"repository"</span><span class="token keyword">:</span> <span class="token punctuation">{</span>
    <span class="token string">"type"</span><span class="token keyword">:</span> <span class="token string">"git"</span>,
    <span class="token string">"url"</span><span class="token keyword">:</span> <span class="token string">"git+https://github.com/jdgamble555/format-price.git"</span>
  <span class="token punctuation">}</span>,
  <span class="token string">"files"</span><span class="token keyword">:</span> <span class="token punctuation">[</span>
    <span class="token string">"dist"</span>
  <span class="token punctuation">]</span>,
  <span class="token string">"type"</span><span class="token keyword">:</span> <span class="token string">"module"</span>,
  <span class="token string">"main"</span><span class="token keyword">:</span> <span class="token string">"dist/index.js"</span>,
  <span class="token string">"scripts"</span><span class="token keyword">:</span> <span class="token punctuation">{</span>
    <span class="token string">"build"</span><span class="token keyword">:</span> <span class="token string">"tsc"</span>,
    <span class="token string">"test"</span><span class="token keyword">:</span> <span class="token string">"vitest run"</span>,
    <span class="token string">"ci"</span><span class="token keyword">:</span> <span class="token string">"npm run build &amp;&amp; npm test"</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><ul><li><code>name</code> - name it</li><li><code>description</code> - describe it</li><li><code>version</code> - use <a target="_blank" href="https://semver.org/">semantic versioning</a> for <a target="_blank" href="https://github.com/changesets/changesets">changesets</a></li><li><code>keywords</code> - add keywords</li><li><code>homepage</code> - add your homepage</li><li><code>bugs</code> - where to report bugs</li><li><code>author</code> - who created this</li><li><code>repository</code> - where to view git source</li><li><code>files</code> - the folder to install</li><li><code>type</code> - use module for TypeScript</li><li><code>main</code> - entry point for node</li><li><code>scripts</code>
 <ul><li><code>build</code> - build TypeScript file</li><li><code>test</code> - test with vitest</li><li><code>ci</code> - continuous integration command for workflow</li></ul></li></ul><h2 id="install-dependencies">Install Dependencies</h2><ul><li><code>npm i -D typescript</code> to install it as development dependency</li><li><code>npm i -D vitest</code> for testing</li></ul><h2 id="config-typescript">Config TypeScript</h2><ul><li>Create <code>tsconfig.json</code> file.</li></ul><pre class=" language-json"><code class="prism  language-json"><span class="token punctuation">{</span>
    <span class="token string">"compilerOptions"</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>
        <span class="token string">"esModuleInterop"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"skipLibCheck"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"target"</span><span class="token punctuation">:</span> <span class="token string">"es2022"</span><span class="token punctuation">,</span>
        <span class="token string">"allowJs"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"resolveJsonModule"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"moduleDetection"</span><span class="token punctuation">:</span> <span class="token string">"force"</span><span class="token punctuation">,</span>
        <span class="token string">"isolatedModules"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"verbatimModuleSyntax"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"strict"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"noUncheckedIndexedAccess"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"noImplicitOverride"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"module"</span><span class="token punctuation">:</span> <span class="token string">"NodeNext"</span><span class="token punctuation">,</span>
        <span class="token string">"outDir"</span><span class="token punctuation">:</span> <span class="token string">"dist"</span><span class="token punctuation">,</span>
        <span class="token string">"rootDir"</span><span class="token punctuation">:</span> <span class="token string">"src"</span><span class="token punctuation">,</span>
        <span class="token string">"sourceMap"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"declaration"</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token string">"declarationMap"</span><span class="token punctuation">:</span> <span class="token boolean">true</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>See <a target="_blank" href="https://www.totaltypescript.com/how-to-create-an-npm-package#32-set-up-a-tsconfigjson">Matt Pocock&rsquo;s Total TypeScript</a> for more info and if you want prettier, etc.</p><h2 id="add-your-typescript-content">Add Your TypeScript Content</h2><p>For this test package, we are updating the price to standard format.</p><ul><li><code>/src/index.ts</code></li></ul><pre class=" language-tsx"><code class="prism  language-tsx">export const formatPrice = (price: number) =&gt; {
    return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
    }).format(price);
};
</code></pre><h3 id="add-testing-script">Add Testing Script</h3><ul><li><code>/src/index.test.ts</code></li></ul><pre class=" language-tsx"><code class="prism  language-tsx">import { formatPrice } from "./index.js";
import { test, expect } from "vitest";

test("formatPrice", () =&gt; {
    expect(formatPrice(1234.56)).toBe("$1,234.56");
});

test("formatPrice with small value", () =&gt; {
    expect(formatPrice(5)).toBe("$5.00");
});
</code></pre><p><strong>Note:</strong> You can test your package with <code>npm run test</code>, which just runs <code>npx vitest run</code> under the hood.</p><h2 id="workflow-1">Workflow 1</h2><p>GitHub has the ability to run scripts once you commit. Every workflow file will be a YAML file with the extension <code>.yml</code>.</p><h3 id="create-default-continuous-integration-workflow">Create Default Continuous Integration Workflow</h3><ul><li><code>.github/workflows/ci.yml</code></li></ul><pre class=" language-json"><code class="prism  language-json">name<span class="token punctuation">:</span> CI

on<span class="token punctuation">:</span>
    pull_request<span class="token punctuation">:</span>
    push<span class="token punctuation">:</span>
        branches<span class="token punctuation">:</span>
            <span class="token operator">-</span> main

concurrency<span class="token punctuation">:</span>
    group<span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> github<span class="token punctuation">.</span>workflow <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">-</span>$<span class="token punctuation">{</span><span class="token punctuation">{</span> github<span class="token punctuation">.</span>ref <span class="token punctuation">}</span><span class="token punctuation">}</span>
    cancel<span class="token operator">-</span><span class="token keyword">in</span><span class="token operator">-</span>progress<span class="token punctuation">:</span> <span class="token boolean">true</span>

jobs<span class="token punctuation">:</span>
    ci<span class="token punctuation">:</span>
        runs<span class="token operator">-</span>on<span class="token punctuation">:</span> ubuntu<span class="token operator">-</span>latest

        steps<span class="token punctuation">:</span>
            <span class="token operator">-</span> uses<span class="token punctuation">:</span> actions<span class="token operator">/</span>checkout@v4

            <span class="token operator">-</span> name<span class="token punctuation">:</span> Use Node<span class="token punctuation">.</span>js
              uses<span class="token punctuation">:</span> actions<span class="token operator">/</span>setup<span class="token operator">-</span>node@v4
              <span class="token keyword">with</span><span class="token punctuation">:</span>
                  node<span class="token operator">-</span>version<span class="token punctuation">:</span> <span class="token string">'20'</span>

            <span class="token operator">-</span> name<span class="token punctuation">:</span> Install dependencies
              run<span class="token punctuation">:</span> npm install

            <span class="token operator">-</span> name<span class="token punctuation">:</span> Run CI
              run<span class="token punctuation">:</span> npm run ci
</code></pre><ul><li>This will install node, install dependencies and run the continuous integration script.</li></ul><h3 id="github">GitHub</h3><p>When you commit your package, you will see the workflow runs in the Action tab on GitHub.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image.png?sfvrsn=ab1ba40b_2" alt="Action tab GitHub" /></p><p>This workflow runs <code>npm run ci</code>, which basically makes sure the tests pass. It is queued first and then run.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-1.png?sfvrsn=112577e2_2" alt="npm run cli" /></p><p>And if it does not pass, you will see an error.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-2.png?sfvrsn=2446f684_2" alt="error" /></p><p>Your last commit will fail.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-3.png?sfvrsn=558b093e_2" alt="commit fail" /></p><p>Which also notifies you in your email.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-4.png?sfvrsn=bbeb2636_2" alt="commit fail email" /></p><p>And, of course, you see a check once you re-commit with tests that pass.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-5.png?sfvrsn=4b9abcdc_2" alt="fixed" /></p><h2 id="versioning-with-changeset">Versioning with ChangeSet</h2><p>Use <a target="_blank" href="https://github.com/changesets/changesets">changesets</a> for versioning.</p><ul><li>Install it with <code>npm i -D @changesets/cli</code></li><li>Initialize it with <code>npx changeset init</code></li></ul><h3 id="create-a-new-version">Create a New Version</h3><ul><li>Run <code>npx changeset</code>
 <ul><li>Select <code>patch</code>, <code>minor</code> or <code>major</code> for the type of update.</li><li>Enter a summary for the log describing the change.</li><li>You will see a new file under <code>.changeset</code> like <code>smooth-ads-lick.md</code> or some random name. This will be the log that will be appended to your root <a target="_blank" href="http://CHANGELOG.md"><code>CHANGELOG.md</code></a> file.</li></ul></li><li>Run <code>npx changeset version</code> to automatically update your package version.
    <ul><li>The generated file <a target="_blank" href="http://smooth-ads-lick.md"><code>smooth-ads-lick.md</code></a> (will be different name) has been deleted, and your log in the root <code>CHANGELOG.md</code> file has been appended.</li></ul></li></ul><pre class=" language-markdown"><code class="prism  language-markdown"><span class="token title important"><span class="token punctuation">#</span> format-price</span>

<span class="token title important"><span class="token punctuation">##</span> 1.0.2</span>

<span class="token title important"><span class="token punctuation">###</span> Patch Changes</span>

<span class="token list punctuation">-</span> testing for log

<span class="token title important"><span class="token punctuation">##</span> 1.0.1</span>

<span class="token title important"><span class="token punctuation">###</span> Patch Changes</span>

<span class="token list punctuation">-</span> first patch
</code></pre><ul><li>Commit to GitHub as is:
    <ul><li><code>git add .</code></li><li><code>git commit -m 'patch'</code></li><li><code>git push</code></li></ul></li></ul><h3 id="github-1">GitHub</h3><p>Now, let&rsquo;s set up for auto publishing.</p><p>Go to <code>Settings</code> in your GitHub project, then click <code>Actions</code> and <code>General</code> on the left menu.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/screenshot_2026-03-07_191810.png?sfvrsn=d7727404_2" alt="Screenshot 2026-03-07 191810.png" /></p><ul><li>You need to allow GitHub to publish to npm with read and write permissions.</li></ul><h3 id="optional-github-ruleset">Optional GitHub Ruleset</h3><p>Under <code>Rules</code> and then <code>Rulesets</code>, it is wise (you will get a warning anyway) to protect your main branch.</p><ul><li>Name the ruleset <code>Restrict Deletions</code> or whatever you like.</li><li>Make sure <code>default</code> is selected under target branches.</li><li>By default, <code>Restrict deletions</code> and <code>Block force pushes</code> will be selected.</li></ul><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-6.png?sfvrsn=b4303860_2" alt="GitHub Ruleset" /></p><h2 id="workflow-2">Workflow 2</h2><p>Create the second workflow file in the <code>.github/workflows</code> folder called <code>release.yml</code>.</p><pre class=" language-markdown"><code class="prism  language-markdown">name: Publish Package

on:
<span class="token code keyword" spellcheck="false">    push:</span>
<span class="token code keyword" spellcheck="false">        branches:</span>
<span class="token code keyword" spellcheck="false">            - main</span>

permissions:
<span class="token code keyword" spellcheck="false">    contents: write # required for tags + version PR</span>
<span class="token code keyword" spellcheck="false">    pull-requests: write # required for Version Packages PR</span>
<span class="token code keyword" spellcheck="false">    id-token: write # required for npm trusted publishing (OIDC)</span>

jobs:
<span class="token code keyword" spellcheck="false">    publish:</span>
<span class="token code keyword" spellcheck="false">        runs-on: ubuntu-latest</span>

<span class="token code keyword" spellcheck="false">        steps:</span>
<span class="token code keyword" spellcheck="false">            - uses: actions/checkout@v4</span>
<span class="token code keyword" spellcheck="false">              with:</span>
<span class="token code keyword" spellcheck="false">                  fetch-depth: 0 # Changesets needs tag + commit history</span>

<span class="token code keyword" spellcheck="false">            - uses: actions/setup-node@v4</span>
<span class="token code keyword" spellcheck="false">              with:</span>
<span class="token code keyword" spellcheck="false">                  node-version: '20'</span>
<span class="token code keyword" spellcheck="false">                  registry-url: 'https://registry.npmjs.org'</span>

<span class="token code keyword" spellcheck="false">            - name: Update npm</span>
<span class="token code keyword" spellcheck="false">              run: npm install -g npm@latest</span>

<span class="token code keyword" spellcheck="false">            - name: Install dependencies</span>
<span class="token code keyword" spellcheck="false">              run: npm ci</span>

<span class="token code keyword" spellcheck="false">            - name: Build (if needed)</span>
<span class="token code keyword" spellcheck="false">              run: npm run build --if-present</span>

<span class="token code keyword" spellcheck="false">            - name: Run tests</span>
<span class="token code keyword" spellcheck="false">              run: npm test</span>

<span class="token code keyword" spellcheck="false">            - name: Changesets version / publish</span>
<span class="token code keyword" spellcheck="false">              uses: changesets/action@v1</span>
<span class="token code keyword" spellcheck="false">              with:</span>
<span class="token code keyword" spellcheck="false">                  # When there *are* pending changesets &rarr; create/update Version Packages PR</span>
<span class="token code keyword" spellcheck="false">                  version: npm run changeset:version</span>

<span class="token code keyword" spellcheck="false">                  # After the Version Packages PR is merged &rarr; publish &amp; create tags</span>
<span class="token code keyword" spellcheck="false">                  publish: npm run changeset:publish</span>
<span class="token code keyword" spellcheck="false">              env:</span>
<span class="token code keyword" spellcheck="false">                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}</span>
<span class="token code keyword" spellcheck="false">                  NPM_CONFIG_PROVENANCE: true</span>
</code></pre><p>This workflow will run the tests with <code>npm test</code>, then create a PR. Once the PR is merged (this is manual for security&mdash;you don&rsquo;t want this automatic), the version is updated, and that version is published to npm.</p><p><strong>Note:</strong> Notice the permissions at the top. We need this to allow changes to the version. This is saved in GitHub under <code>Releases</code> and <code>Tags</code>.</p><h2 id="publish-to-npm">Publish to npm</h2><p>This assumes you have an npm account. You can create one at <a target="_blank" href="https://www.npmjs.com/">npm</a> if you don&rsquo;t have one. Make sure to enable 2FA.</p><p>Publish your first version to npm by running.</p><ul><li><code>npm login</code> &ndash; You will log in with the browser, then enter your 2FA code.</li><li>Run <code>npm publish --access public</code> to publish a public package.</li></ul><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-7.png?sfvrsn=62b98a10_2" alt="npm 11 packages" /></p><p>Once you publish, you will see your package published in npm. This means anyone can install it with <code>npm i format-price</code> in their repo.</p><p><strong>Note:</strong> Our final goal is to publish automatically with a git commit.</p><h3 id="configure-github-publishing">Configure GitHub Publishing</h3><p>Click on the package, then go to <code>Settings</code>.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-8.png?sfvrsn=f8e3f7da_2" alt="Settings" /></p><ul><li>Create a new Connection to GitHub Actions.</li></ul><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-9.png?sfvrsn=fc949920_2" alt="Trusted Publisher page with fields for org or user, repo, workflow filename, environment name - to set up new connection" /></p><ul><li>Enter your GitHub username and repository, as well as your YAML file <code>release.yml</code>.</li></ul><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-10.png?sfvrsn=d5c36c46_2" alt="prepped trusted publisher can be edited or deleted" /></p><ul><li>You will see a connection enabled for GitHub to be a Trusted Publisher.</li></ul><h3 id="auto-publishing">Auto Publishing</h3><p>Now that the connection is setup, you can publish automatically.</p><ul><li>Create a change to your file.</li><li>Run <code>npm run build</code> and then whatever other formatting or pre-publishing commands you use.</li><li>Run <code>npx changeset</code> to create a new version. You can use <code>patch</code> for testing.</li><li>Create a new commit
    <ul><li><code>git add .</code></li><li><code>git commit -m 'auto publish'</code></li><li><code>git push</code></li></ul></li><li>Go to your project on GitHub, and you should see a successful Pull Request.</li></ul><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-11.png?sfvrsn=1ff07600_2" alt="Pull requests - 1- version packages" /></p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-12.png?sfvrsn=1eac6d67_2" alt="successful pull request - no conflicts with base branch" /></p><ul><li>Click the <code>Merge pull request</code> button to merge the versioning and changes. It will also auto publish and auto update your versions. Feel free to add any more description info for the PR.</li><li>Wait a few minutes for the publish to update, and you will now see a <code>Releases</code> option in your GH sidebar.</li></ul><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-13.png?sfvrsn=a85f1001_2" alt="GitHub sidebar includes Releases" /></p><ul><li>Don&rsquo;t forget to pull the version changes locally with <code>git pull</code>.</li></ul><p><strong>You now have a fully automated npm publishing pipeline!</strong></p><p>You can install your package anywhere with <code>npm i format-price</code>!</p><p><strong>Example Repo:</strong> <a target="_blank" href="https://github.com/jdgamble555/format-price">GitHub</a></p><hr /><p><strong>Read next:</strong> <a target="_blank" href="https://www.telerik.com/blogs/typedb-graph-database-things">TypeDB: A Graph Database for Things</a></p><img src="https://feeds.telerik.com/link/10827/17352916.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:5c3a107b-0444-4d8f-a104-97f53edd0760</id>
    <title type="text">How a 6-Person Team Shipped an AI-First Platform with KendoReact</title>
    <summary type="text">For teams building AI-driven applications with complex workflows, treating the UI layer as infrastructure can dramatically reduce friction as the product evolves. For Icanpreneur, KendoReact became that foundation.</summary>
    <published>2026-05-28T14:47:38Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Kathryn Grayson Nanz </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17350156/how-a-6-person-team-shipped-an-ai-first-platform-with-kendoreact"/>
    <content type="text"><![CDATA[<p><span class="featured">For teams building AI-driven applications with complex workflows, treating the UI layer as infrastructure can dramatically reduce friction as the product evolves. For Icanpreneur, KendoReact became that foundation.</span></p><p>Wiring up an LLM endpoint takes an afternoon; the harder engineering problem is building a UI layer that can absorb the inherent unpredictability of AI without introducing layout thrashing, inconsistent interaction patterns or sprawl that a small team can't maintain. </p><p>That's the problem Icanpreneur solved with KendoReact. </p><blockquote>&ldquo;The hard part with AI is not just calling a model &ndash; it&rsquo;s designing complex, trustworthy workflows around it. That&rsquo;s where KendoReact helped a lot.&rdquo; </blockquote><p><a href="https://www.icanpreneur.com/">Icanpreneur&rsquo;s platform</a> orchestrates guided, AI-assisted workflows that blend business logic, structured data and real-time feedback into a familiar, approachable experience. Users move through Lean Canvas modeling, validation flows and strategic planning steps with AI augmenting their thinking along the way. They&rsquo;re now used not only by early-stage founders but also by accelerators, innovation hubs and product teams inside organizations such as Founder Institute, Campus X, Science Park Graz, ABLE Activator, Sofia Tech Park, Visa Innovation Program Europe and Telerik Academy&rsquo;s Upskill Product Management program. </p><p>Raw AI output can be unpredictable. Without a stable, consistent UI foundation, that translates into friction and mistrust. The consistency, predictability and performance of <a href="https://www.telerik.com/kendo-react-ui">KendoReact</a> in the UI layer turned the output of Icanpreneur&rsquo;s AI assistant, IVA, into something usable and trustworthy. For Icanpreneur&rsquo;s six-person team, KendoReact was the infrastructure that made AI usable at scale. </p><h2>Icanpreneur&rsquo;s Architecture </h2><p>At a high level, Icanpreneur&rsquo;s platform is structured as a layered system that separates UI composition and user interaction, server / API management and LLM orchestration.</p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/.net-maui-aiprompt/screenshot-2026-05-28-at-9-44-37-am.png?sfvrsn=127900_2" height="844" style="max-width:100%;height:auto;" title="Screenshot 2026-05-28 at 9.44.37 AM" width="1016" alt="A graphic depiction of the frontend architecture " sf-size="411556" /><p>KendoReact is the core of the Icanpreneur UI layer, allowing them to guide users through complex multi-pane screens, support long-running workflows with consistent UI patterns and handle advanced multi-step journeys without overwhelm. Every major module (Lean Canvas, conversational interview console, go-to-market editor, persona builder) is composed from the same KendoReact primitive set, themed consistently and governed by the same layout contracts. That decision paid compounding dividends as the product scaled. </p><p>In an AI-first platform, it can be tempting to start thinking about the UI as set dressing; just a thin layer over the APIs to make things &ldquo;look pretty&rdquo; for the users. However, AI interaction patterns are still very new and unfamiliar to many users. A UI that naturally folds AI into the user experience can be a real differentiator in the competitive market. </p><blockquote>&ldquo;Founders in partner programs started telling us that the interview, insights and go-to-market flow &lsquo;feels like one tool &ndash; simple and intuitive, not five stitched together.'&rdquo; </blockquote><p>AI technology is impressive but UI engineers are still the ones who translate that potential into true value for the user. In Icanpreneur&rsquo;s case, they needed stable layout primitives, reliable form controls and high-performance data visualization components &ndash; all of which had to present AI output reliably (while responses were streaming, partial or evolving) without triggering unnecessary re-renders or layout shifts. KendoReact provided that and more, empowering the team to focus their effort on user experience, business logic and AI orchestration. </p><h3>Composability as a Force Multiplier </h3><p>One of the most important architectural decisions was treating KendoReact not as a collection of finished widgets or mere building blocks to be combined but as true UI infrastructure. KendoReact powers everything from research dashboards and conversational interview consoles to mini-CRMs and multi-step go-to-market editors.</p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/.net-maui-aiprompt/screenshot-2026-05-28-at-9-45-10-am.png?sfvrsn=1b568e58_2" height="802" style="max-width:100%;height:auto;" title="Screenshot 2026-05-28 at 9.45.10 AM" width="1370" alt="A screenshot of the Icanpreneur interface, built with KendoReact components" sf-size="418054" /><p>Foundation pieces were combined to create higher-order workflows that guide users through their interactions with IVA. Rather than building custom UI for each flow, the team defined composable patterns built on KendoReact primitives. For example, the multi-step validation flow reused the same layout + navigation structure, AI feedback panels reused consistent container and typography patterns and interview summaries reused standardized layout and card structures. Because the Icanpreneur team didn&rsquo;t need to design complex new interaction patterns for each additional feature, they were able to implement quickly and iterate fast &ndash; smoothly layering their AI workflows on top of KendoReact&rsquo;s component system. </p><h3>Consistent UX for Novel AI Workflows </h3><p>For Icanpreneur users, this meant that they never had to open a page and feel unsure of where to go or what to do next &ndash; even though the AI-powered technology may be new, it leveraged familiar and consistent patterns to guide them through the experience. </p><blockquote>&ldquo;Because all the AI-driven experiences reuse the same KendoReact components as the rest of the app, they behave in a predictable way. Users don&rsquo;t have to &lsquo;learn' a new interface just because AI is involved &ndash; it feels like one coherent workspace.&rdquo; </blockquote><h3>Design-to-Code Fidelity </h3><p>With KendoReact, Icanpreneur designers and engineers worked from the same component language, which meant no translation layer between design and implementation, no pixel-chasing and no divergence between what's mocked and what ships. Designers also created a custom design system using the <a href="https://www.telerik.com/figma-kits">Kendo UI Figma Kits</a> and the <a href="https://www.telerik.com/design-system/docs/">Progress Design System Kit</a>, which greatly reduced the time needed to create mockups and new pages. </p><blockquote>&ldquo;Using the Kendo UI Figma Kits and a custom Kendo theme, we aligned design and development from day one. Most new features now start as a quick sketch in our KendoReact-based design system and turn into a working screen in days instead of weeks.&rdquo; </blockquote><h3>Increased Development Speed </h3><p>AI-assisted workflows evolve quickly: new steps get added, feedback formats change and validation criteria expand. Because KendoReact components are extensible and <a href="https://www.telerik.com/kendo-react-ui/components/styling">themeable</a>, the Icanpreneur team could meet these challenges while still preserving UX consistency. As workflows grew, the UI layer remained adaptable; less time debugging or re-writing UI logic meant faster revision cycles and more shipped features. </p><blockquote>&ldquo;New workflow-style features (such as a new research flow or AI-assisted editor) now typically go from idea to shipped version in days instead of weeks, because we mostly compose existing KendoReact patterns instead of building UI from scratch.&rdquo; </blockquote><h2>The UX of AI </h2><p>Some of the biggest challenges in AI-first applications are handling uncertainty (usually in the form of partial / still evolving responses) and guiding users through new workflows. The Icanpreneur team leveraged KendoReact&rsquo;s design tools to create UX patterns that integrate AI feedback into existing, structured UI flows, so users are never left wondering &ldquo;what now?&rdquo;.</p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/.net-maui-aiprompt/screenshot-2026-05-28-at-9-46-59-am.png?sfvrsn=e6e4d053_2" height="836" style="max-width:100%;height:auto;" title="Screenshot 2026-05-28 at 9.46.59 AM" width="1370" alt="A screenshot of the Icanpreneur interface, built with KendoReact components" sf-size="477986" /><p>One of the most unique features of Icanpreneur is their Synthetic Customer Interview feature, which allows their AI assistant, IVA, to answer questions as though it was a potential customer in their target market. Not all teams have easy access to customers to run interviews with, so this allows founders to &ldquo;stress-test&rdquo; their hypotheses quickly across multiple scenarios. That output helps them refine their questions and assumptions before talking to real people. Afterwards, IVA reviews all the data (across both real and synthetic customer interviews) to summarize, highlight patterns and extract quotes and evidence that can be leveraged in personas and further market research. </p><blockquote>&ldquo;When we designed IVA, the conversational interview console and the research workspace, we could prototype and ship quickly because we already had chat-style layouts built from existing KendoReact <a href="https://www.telerik.com/kendo-react-ui/components/layout">Layout</a> and <a href="https://www.telerik.com/kendo-react-ui/components/form">Form</a> components, multi-step flows for things like research setup and go-to-market editors and reusable <a href="https://www.telerik.com/kendo-react-ui/components/layout/expansionpanel">Panels</a>, <a href="https://www.telerik.com/kendo-react-ui/components/layout/drawer">Drawers</a>, <a href="https://www.telerik.com/kendo-react-ui/components/dialogs/dialog">Dialogs</a> and <a href="https://www.telerik.com/kendo-react-ui/components/grid">Data Grids</a> for displaying AI outputs, suggestions and insights aggregation&rdquo; </blockquote><p>Using KendoReact meant that not only could they leverage these familiar user patterns &ndash; but also that common AI concerns (such as slow, partial or unexpected responses) could be handled with UI structures and error responses that users already knew how to interact with. </p><h2>Performance and Scalability </h2><p>Icanpreneur workflows are complex, multi-stage journeys. Moving from idea to hypothesis, validating with AI or human-led interviews, identifying patterns and extracting valuable feedback, generating personas and finally creating landing pages or pitch decks &ndash; any one of these alone would be demanding but all together they offer a true development challenge. Each step builds upon the previous and the context must be preserved as the user moves between them. Without careful performance engineering, rendering and re-rendering these views would quickly degrade the user experience. </p><p>AI workflows can introduce frequent state updates as responses stream in or evolve - for example, when IVA synthesizes interview insights or drafts positioning. In poorly structured UIs, this can lead to layout thrashing or lag. However, these updates render inside structured, well-optimized KendoReact components, so the Icanpreneur UI remains stable.</p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/.net-maui-aiprompt/screenshot-2026-05-28-at-9-47-32-am.png?sfvrsn=c831f8dc_2" height="818" style="max-width:100%;height:auto;" title="Screenshot 2026-05-28 at 9.47.32 AM" width="1380" alt="A screenshot of the Icanpreneur interface, built with KendoReact components" sf-size="752777" /><p>As features accumulate, custom CSS and one-off component implementations are a common source of bundle bloat and regression risk. At Icanpreneur, new features and modules all plug into the same KendoReact UI system (rather than introducing new patterns and components). That allows even a small team to control UI sprawl and manage bundle growth. Custom CSS &ndash; a common pain point for fast-growing applications &ndash; is significantly reduced and centralized, because the UI is themed consistently. This kept initial load times reasonable even as the feature surface expanded. </p><blockquote>&ldquo;When we introduce new AI-driven experiences, they use the same KendoReact components, so we see far fewer UI regressions. That made it much easier to roll out new AI features without exploding our QA surface.&rdquo; </blockquote><p>That smaller, bounded QA surface meant faster turnaround and faster time to ship, while the lower complexity means that the small team was able to manage the expedited growth without being overwhelmed. New features that reuse existing KendoReact components inherit known-good behavior, so regression risk didn't scale with feature additions. </p><p>KendoReact&rsquo;s components are designed for high-density, data-heavy enterprise applications; no reinvention (or re-optimization) was required even for complex components like grids, forms and dialogs. KendoReact reduced the need to solve hard performance problems manually, allowing Icanpreneur to access enterprise-level quality with a startup-size team. </p><h2>Icanpreneur: Powered by KendoReact </h2><p>Icanpreneur began as a structured way to guide founders from idea to product-market fit. Today it operates as a full AI co-founder, with IVA empowering entrepreneurs to ideate, validate and grow their companies as a trusted partner. </p><p>By treating the UI layer as infrastructure and building on KendoReact from day one, the team was able to: </p><ul><li>Scale complex, AI-driven workflows with consistency </li></ul><ul><li>Ship new features rapidly without fragmenting UX </li></ul><ul><li>Deliver a coherent experience across classic and AI-powered screens </li></ul><p>For teams building AI-driven workflows, the UI layer is crucial: it's what determines whether AI output becomes a usable, trustworthy product or a source of friction. Icanpreneur's architecture is a case study in treating that layer seriously from day one. Or, as the Icanpreneur team said themselves: </p><blockquote>&ldquo;A six-person core team is able to maintain and evolve a fairly large, AI-first product (research, interviews, personas, positioning, landing pages, sales decks, etc.) without a separate &ldquo;component team&rdquo; or design system squad &ndash; KendoReact is that system for us.&rdquo; </blockquote><p>For teams building AI-driven applications with complex workflows, treating the UI layer as infrastructure can dramatically reduce friction as the product evolves. For Icanpreneur, KendoReact became that foundation: explore how KendoReact could become that foundation for your team, as well.</p><img src="https://feeds.telerik.com/link/10827/17350156.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:9941dce1-d02d-4fcd-9297-f3cd46f3ea42</id>
    <title type="text">Querying Reliable AI Resources with Telerik Agent Tools API</title>
    <summary type="text">Telerik Document Processing Libraries not only let you create RAG resources for your LLMs. They let you integrate those libraries into Microsoft’s latest agent-based tools.</summary>
    <published>2026-05-27T14:04:51Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Peter Vogel </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17349392/querying-reliable-ai-resources-telerik-agent-tools-api"/>
    <content type="text"><![CDATA[<p><span class="featured">Telerik Document Processing Libraries not only let you create RAG resources for your LLMs. They let you integrate those libraries into Microsoft&rsquo;s latest agent-based tools.</span></p><p>In my previous post, I showed how to use Progress <a href="https://www.telerik.com/blogs/creating-reliable-ai-resources-with-telerik-agent-tools-api" target="_blank">Telerik Agent Tools API to create a workflow</a>&nbsp;that loads content into a Resource-Augmented Generation (RAG) resource that can be used with Large Language Models (LLMs). That RAG resource, integrated with an LLM in your application, helps your users get reliable, grounded answers, driven by the content you load into your resource.</p><p>This post shows how to tie your RAG resource to an LLM and integrate the combination in an application that allows users to query the resource&mdash;or any combination of resources that you&rsquo;ve created. Specifically, I&rsquo;ll show how to use Microsoft&rsquo;s current technology for querying an AI resource, the <code>ChatClientAgent</code> object.</p><p>Telerik Agent Tools API provides a collection of toolsets that the chat client agent works with to query your RAG resources. You just have to load your RAG resource(s) into in-memory repositories, attach the appropriate toolsets and pass the resulting tools to a chat client agent. Once you&rsquo;ve done that, you can submit prompts to the agent and get back the results driven by the RAG resources you&rsquo;ve loaded.</p><h2 id="configuring-your-project">Configuring Your Project</h2><p>To create an application that can query your resources, you first need the <a target="_blank" href="http://Telerik.Documents.AI">Telerik.Documents.AI</a>.* NuGet packages that work with the documents in your RAG resource (I covered those <a href="https://www.telerik.com/blogs/creating-reliable-ai-resources-with-telerik-agent-tools-api#configuring-your-project" target="_blank">libraries in my previous post</a>).</p><p>After that, you also need two Microsoft AI NuGet packages:</p><ul><li>Azure.AI.OpenAI</li><li>Microsoft.Agents.AI.OpenAI</li></ul><p>I used an ASP.NET Web API project for my case study so I also needed to add the Microsoft.AspNetCore.OpenApi package.</p><p>As I write this, the Microsoft.Agents.AI.OpenAI package was in &ldquo;release candidate&rdquo; mode which means that, while its interfaces and functionality are fixed, I needed to use the &ldquo;prerelease&rdquo; option when adding the package. Having said that, by the time you read this, the package may have moved to &ldquo;latest stable&rdquo; status (this is a <em>very</em> agile environment).</p><h3 id="configuring-a-chat-client-agent">Configuring a Chat Client Agent</h3><p>Before you create your <code>ChatClientAgent</code> agent object, you need to assemble a set of tools, tied to one or more of your RAG resources. Your first step is to create a repository of the right type (PDF, spreadsheet, Word/Word-related) and load the file that holds your RAG resource into that repository.</p><p>For my case study, I&rsquo;m only working with PDF documents&mdash;what Progress Telerik calls &ldquo;Fixed&rdquo; documents&mdash;so I created a <code>IFixedDocumentRepository pdfRepo</code> repository (all the repositories share a common interface so they all look very much alike).</p><p>Once I created the repository, I loaded my RAG resource using the repository&rsquo;s <code>Import</code> method. The <code>Import</code> method must be passed a <code>FileStream</code> object pointing to the resource and its format which, in this case, was <code>DocumentFormat.PDF</code> (see my previous post for the details on creating that resource).</p><p>Here&rsquo;s the code that loads my case study&rsquo;s repository:</p><pre class=" language-csharp"><code class="prism  language-csharp">IFixedDocumentRepository pdfRepo <span class="token operator">=</span> 
               <span class="token keyword">new</span> <span class="token class-name">InMemoryFixedDocumentRepository</span><span class="token punctuation">(</span> <span class="token keyword">new</span> <span class="token class-name">TimeSpan</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span> Path<span class="token punctuation">.</span><span class="token function">Exists</span><span class="token punctuation">(</span>repoPath<span class="token punctuation">)</span> <span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">string</span> name <span class="token operator">=</span>pdfRepo<span class="token punctuation">.</span><span class="token function">Import</span><span class="token punctuation">(</span>
                                        <span class="token function">FileStream</span><span class="token punctuation">(</span>repoFilePath<span class="token punctuation">,</span> FileMode<span class="token punctuation">.</span>Open<span class="token punctuation">,</span> FileAccess<span class="token punctuation">.</span>Read<span class="token punctuation">)</span><span class="token punctuation">,</span>
                                       DocumentFormat<span class="token punctuation">.</span>PDF<span class="token punctuation">,</span>
         Path<span class="token punctuation">.</span><span class="token function">GetFileNameWithoutExtension</span><span class="token punctuation">(</span>repoFilePath<span class="token punctuation">)</span>
                                     <span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>If you provide the name of the file as the third parameter, the <code>Import</code> method returns that name (and returns &ldquo;ImportedDocument&rdquo; if you don&rsquo;t provide the third parameter).</p><p>To support having the chat client agent query my PDF repository, I just need the <code>FixedDocumentContentAgentTools</code> toolset. To create that toolset, I have to pass two parameters: the repository itself and a folder that holds any images used in those PDF documents. When attaching a toolset to a repository, you&rsquo;ll always have to pass the repository parameter, but other toolsets will require different parameters (and, often, no other parameters).</p><p>Once I&rsquo;ve created that toolset, I extract its tools using the toolset&rsquo;s <code>GetTools</code> method and add those tools to a list of <code>AITool</code> objects that will, eventually, be passed to my chat client agent. The code to do that looks like this:</p><pre class=" language-csharp"><code class="prism  language-csharp">pdfReadTools <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FixedDocumentContentAgentTools</span><span class="token punctuation">(</span>pdfRepo<span class="token punctuation">,</span> repoPath<span class="token punctuation">)</span><span class="token punctuation">;</span>
List<span class="token operator">&lt;</span>AITool<span class="token operator">&gt;</span> queryTools <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">List</span><span class="token operator">&lt;</span>AITool<span class="token operator">&gt;</span><span class="token punctuation">(</span> pdfReadTools<span class="token punctuation">.</span><span class="token function">GetTools</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>If I wanted to add more tools to my list, I would use the list&rsquo;s <code>AddRange</code> method. As an example, the following code:</p><ul><li>Creates a repository for holding spreadsheets</li><li>Imports a RAG resource file of spreadsheets into that repository using the repository&rsquo;s <code>Import</code> method</li><li>Attaches the <code>SpreadProcessingReadAgentTools</code> toolset (which only needs to be passed a reference to the repository)</li><li>Extracts the toolset&rsquo;s tools and adds them to my list of <code>AITools</code></li></ul><pre class=" language-csharp"><code class="prism  language-csharp">IWorkbookRepository workbookRepo <span class="token operator">=</span> 
              <span class="token keyword">new</span> <span class="token class-name">InMemoryWorkbookRepository</span><span class="token punctuation">(</span> <span class="token keyword">new</span> <span class="token class-name">TimeSpan</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
workbookRepo<span class="token punctuation">.</span><span class="token function">Import</span><span class="token punctuation">(</span>workbookRepoPath<span class="token punctuation">,</span> DocumentFormat<span class="token punctuation">.</span>XLSX<span class="token punctuation">)</span><span class="token punctuation">;</span>

SpreadProcessingReadAgentTools workbookTools <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span>workbookRepo<span class="token punctuation">)</span><span class="token punctuation">;</span>

queryTools<span class="token punctuation">.</span><span class="token function">AddRange</span><span class="token punctuation">(</span> workbookTools<span class="token punctuation">.</span><span class="token function">GetTools</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><h2 id="querying-your-repositories">Querying Your Repositories</h2><p>Now that I have a list of tools, I&rsquo;m ready to create an agent. There are three steps to doing that (but you can do it in one line of code).</p><p>First, you need to create a <code>AzureOpenAIClient</code> object which, in its simplest form, just requires passing the URL for the <a target="_blank" href="https://www.telerik.com/blogs/creating-custom-ai-agent-telerik-tools-1-configuring-llm-azure-ollama">LLM deployment you&rsquo;ve created</a> and the authorization key for that deployment.</p><p>One note: Using an authorization key is probably fine for development but, in production, you should be authorizing access using something more robust (e.g., <a target="_blank" href="https://www.telerik.com/blogs/coding-azure-4-securing-web-service-app-service-access-azure-sql-database">Managed Identities in Entra ID</a>). If you&rsquo;re using an authentication key, then you shouldn&rsquo;t hardcode into your application as I do here but move that key to some more secure location (e.g. An <a target="_blank" href="https://www.telerik.com/blogs/coding-azure-12-configuring-azure-key-vault-adding-secrets">Azure Key Vault</a>).</p><p>Once you&rsquo;ve created your <code>AzureOpenAIClient</code> object, your second step is to call its <code>GetChatCient</code> method to configure and return a <code>ChatClient</code> object. The <code>GetChatClient</code> method just needs to be passed the name of the deployment you created for your LLM.</p><p>Finally, you need to call your <code>ChatClient</code> object&rsquo;s <code>AsAIAgent</code> method to configure your agent. You can pass a variety of parameters as part of configuring your agent. I settled for specifying a name for my agent, some instructions on how my agent is to answer questions, and my list of tools:</p><pre class=" language-csharp"><code class="prism  language-csharp">AIAgent agent <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AzureOpenAIClient</span><span class="token punctuation">(</span>
                                      <span class="token keyword">new</span> <span class="token class-name">Uri</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">,</span>
                                      <span class="token keyword">new</span> <span class="token class-name">AzureKeyCredential</span><span class="token punctuation">(</span>apiKey<span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">GetChatClient</span><span class="token punctuation">(</span>deploymentName<span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">AsAIAgent</span><span class="token punctuation">(</span>
                instructions<span class="token punctuation">:</span> <span class="token string">"You provide guidance to Azure software developers"</span><span class="token punctuation">,</span>
                name<span class="token punctuation">:</span> <span class="token string">"Async App Expert"</span><span class="token punctuation">,</span>
                tools<span class="token punctuation">:</span> queryTools<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>With your chat client agent in hand, you process your user&rsquo;s prompts by calling the agent&rsquo;s <code>RunAsync</code> method and passing the prompt. The agent will return an <code>AgentResponse</code> object which has a <code>Messages</code> collection holding a list of responses drawn from the repositories associated with your tools (you probably want the first message). The <code>Text</code> property on a message will give you the agent&rsquo;s response.</p><p>Typical code would look like this:</p><pre class=" language-csharp"><code class="prism  language-csharp">AgentResponse response <span class="token operator">=</span> <span class="token keyword">await</span> agent<span class="token punctuation">.</span><span class="token function">RunAsync</span><span class="token punctuation">(</span>"What <span class="token keyword">do</span> NuGet packages <span class="token keyword">do</span> I need&rdquo;<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">string</span> responseText <span class="token operator">=</span> response<span class="token punctuation">.</span>Messages<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>Text<span class="token punctuation">;</span>
</code></pre><h2 id="interacting-with-your-rag-enabled-sources">Interacting with Your RAG-Enabled Sources</h2><p>Of course, you&rsquo;ll also want to create a UI for your users to interact with when querying your RAG resource. In earlier posts, I showed how to leverage Telerik tools to create dedicated user-friendly <a href="https://www.telerik.com/blogs/creating-custom-ai-agent-telerik-tools-4-crafting-interactive-blazor-ui" target="_blank">UIs in Blazor</a>&nbsp;or <a href="https://www.telerik.com/blogs/creating-custom-ai-agent-telerik-tools-5-creating-interactive-ui-javascript" target="_blank">JavaScript</a>. Alternatively, instead of creating a UI dedicated to your RAG-enabled resource, you might want to more tightly <a href="https://www.telerik.com/blogs/creating-custom-ai-agent-telerik-tools-6-embedding-conversational-invisible-agents" target="_blank">integrate your resource into a JavaScript application&rsquo;s UI</a>.&nbsp;</p><p>But, really, it&rsquo;s up to you how you&rsquo;ll use your RAG resource to support your users.</p><aside><hr data-sf-ec-immutable="" /><div class="row"><div class="col-4 u-normal-full u-small-mb0"><h4 class="u-fs20 u-fw5 u-lh125 u-mb0">Get access to these tools and more</h4></div><div class="col-8"><p class="u-fs16 u-mb0">The free 30-day trial of <a target="_blank" href="https://www.telerik.com/devcraft">Telerik DevCraft</a> lets you really kick the tires for yourself. <a target="_blank" href="https://www.telerik.com/try/devcraft-ultimate">Try it today!</a></p></div></div></aside><img src="https://feeds.telerik.com/link/10827/17349392.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:3c1b27d0-6ddf-4b4f-8063-d9d6d3024ec6</id>
    <title type="text">From DevUI to Observability: Evolving the Agent Dev Loop in Microsoft Agent Framework</title>
    <summary type="text">This article shows .NET developers how to get started building and debugging agent-based applications locally with Microsoft Agent Framework and DevUI. The article also explores how DevUI shortens the agent development loop by providing a visual interface for running workflows and inspecting agent interactions. This can now be addressed with the Progress AI Observability Platform.</summary>
    <published>2026-05-26T19:41:09Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Ed Charbeneau </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17352185/from-devui-to-observability-evolving-the-agent-dev-loop-in-microsoft-agent-framework"/>
    <content type="text"><![CDATA[<p><span class="featured">This article shows .NET developers how to get started building and debugging agent-based applications locally with Microsoft Agent Framework and DevUI. The article also explores how DevUI shortens the agent development loop by providing a visual interface for running workflows and inspecting agent interactions. This can now be addressed with the Progress AI Observability Platform.</span></p><p>DevUI is a lightweight standalone application included with Microsoft Agent Framework that helps developers run, inspect, and debug agents and workflows during development. The tool provides a web-based interface for interactive testing alongside an OpenAI-compatible API backend, making it easier to iterate on workflows before integrating them into a larger application.</p><p>The key to DevUI&rsquo;s usefulness is its ability to shorten the agent development loop by giving developers immediate visibility into workflow execution and agent interactions. However, as agentic applications move from prototype to production, a new observability gap begins to appear. Developers need to understand not only what happened during a local debugging session, but how agents behave across users, sessions, models, tools, latency, cost, quality, and failures.</p><p>In this article, you'll use DevUI to develop, visualize, and debug workflows locally before expanding into observability to support multi-session debugging and production-ready diagnostics. By extending the agent development loop beyond local testing, observability helps bridge the gap between development-time insight and the operational visibility required for production systems.</p><h2 id="installing-the-microsoft-agent-framework-templates">Installing the Microsoft Agent Framework Templates</h2><p>To get started with DevUI and Microsoft Agent Framework, install the project templates from NuGet. The templates provide a starter project for building and debugging agent-based applications in .NET.</p><p>Install the templates using the .NET CLI:</p><pre><code class="language-bash">dotnet new install Microsoft.Agents.AI.ProjectTemplates::1.3.0-preview.1.26251.3
</code></pre><p>Once installed, the templates become available through Visual Studio under <strong>File &gt; New Project</strong>. Search for <code>Agent</code> to locate the available Microsoft Agent Framework project templates, shown in Figure 1.</p><p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/observability/ai-agent-template.png?sfvrsn=27c6cc2b_2" height="457" style="max-width:100%;height:auto;" width="800" alt="" /></p><p><strong>Figure 1: </strong>The project template shown in Visual Studio.</p><p>You can also create a new project directly from the command line:</p><pre><code class="language-bash">dotnet new aiagent-webapi
</code></pre><p>The generated project includes a starter implementation configured for Microsoft Agent Framework and DevUI, making it easy to begin experimenting with agents, orchestration, and workflow debugging locally.</p><h3 id="exploring-the-template">Exploring the Template</h3><p>The <code>aiagent-webapi</code> template includes a complete sample application that demonstrates how agents and workflows operate within Microsoft Agent Framework.</p><p>The sample application contains two hosted agents and a workflow:</p><ol><li><p><strong>Writer Agent</strong><br />Generates short stories based on a provided topic while keeping responses under 300 words.</p></li><li><p><strong>Editor Agent</strong><br />Reviews and refines the generated story by improving grammar, readability, and style while maintaining the word limit.</p></li><li><p><strong>Publisher Workflow Agent</strong><br />Coordinates the workflow between the writer and editor agents using a sequential process that passes content through each stage of execution.</p></li></ol><p>The agents are exposed through OpenAI-compatible API endpoints, making the sample easy to test with DevUI and simple to integrate with external tools and applications. By keeping the architecture approachable, the template creates a practical environment for understanding how agent orchestration works in a real .NET application.</p><h3 id="understanding-addagent-and-ihostedagentbuilder">Understanding <code>AddAgent</code> and <code>IHostedAgentBuilder</code></h3><p>Microsoft Agent Framework registers agents using the <code>AddAgent</code> extension method during application startup. This approach integrates agents directly into the standard ASP.NET Core dependency injection system, making the programming model feel familiar to .NET developers.</p><pre><code class="language-csharp">builder.AddAIAgent("writer", 
    "You write short stories (300 words or less) about the specified topic.");
</code></pre><p>The <code>AddAgent</code> method returns an <code>IHostedAgentBuilder</code>, which provides additional configuration options for the agent and its hosting behavior. The template uses this pattern to register the Writer, Editor, and Publisher workflow agents so they can be discovered and executed through DevUI and the framework&rsquo;s OpenAI-compatible endpoints.</p><h2 id="running-the-application">Running the Application</h2><p>With the project created, the next step is running the application and launching DevUI. Start the application using the .NET CLI:</p><pre><code class="language-bash">dotnet run
</code></pre><p>The application exposes OpenAI-compatible API endpoints that can be accessed from compatible clients and tools. During development, the application also maps a <code>/devui/</code> route that launches the Agent Framework development UI.</p><p>When running the project from Visual Studio or another IDE, the browser automatically opens to the DevUI endpoint. DevUI provides a web-based interface for interacting with agents and workflows while using the framework&rsquo;s Responses and Conversations endpoints behind the scenes. This creates a fast feedback loop for testing prompts, tracing interactions, and validating workflow execution during development.</p><h2 id="understanding-the-agent-development-loop">Understanding the Agent Development Loop</h2><p>DevUI improves the agent development loop by making it easier to build, run, inspect, and refine workflows during development. Instead of treating agents as black-box processes, developers can interact with workflows in real time and observe how messages move between agents during execution.</p><p>Both agents and workflows can be independently selected from the interface, shown in Figure 2.</p><p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/observability/agent-workflow-selection-steps.png?sfvrsn=a4dd7e97_2" height="479" style="max-width:100%;height:auto;" width="600" alt="" /></p><p><strong>Figure 2: </strong>The browser running DevUI with the agent selection showing the available agents and workflows. 1) The editor and writer agents can be selected and executed independently. 2) The publisher workflow can be executed.</p><p>This visibility becomes increasingly important as workflows grow more complex. Multi-agent systems introduce challenges that traditional request-response applications typically avoid, including non-deterministic behavior, chained execution steps, and state shared across conversations.</p><p>To run a selected workflow in DevUI, open a prompt by clicking Configure and Run. Then enter a prompt in the dialog box, shown in Figure 3. The prompt text is passed into the workflow and the entire workflow will execute when Run Workflow is clicked.</p><p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/observability/run-workflow.png?sfvrsn=7dce9b17_2" height="355" style="max-width:100%;height:auto;" width="500" alt="" /></p><p><strong>Figure 3: </strong>The Configure Workflow Input dialog box.</p><p>DevUI creates an approachable debugging experience, its current model focuses on short-lived, single-session workflows. The final output is clearly displayed on screen, shown in Figure 4.</p><p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/observability/workflow-complete.png?sfvrsn=906d9d62_2" height="584" style="max-width:100%;height:auto;" width="500" alt="" /></p><p><strong>Figure 4: </strong>The output shown in the workflow execution timeline.</p><p>This works well for experimentation and local testing. However, debugging becomes more difficult once multiple sessions are running concurrently or workflows require historical visibility for troubleshooting.</p><p>For example, the execution information is shown for this run in the Events panel in Figure 5. While the status and token information is visible from DevUI, it does not persist once the application stops.</p><p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/.net-maui-aiprompt/events.png?sfvrsn=a0b2a54e_2" height="431" style="max-width:100%;height:auto;" width="395" alt="" /></p><p><strong>Figure 5:</strong> The Events panel displays a completed status along with usage statistics for token consumption.</p><p>As developers move beyond isolated testing scenarios, the agent development loop begins to require more than interactive debugging alone. Understanding how workflows behave across sessions, tracing execution over time, and diagnosing failures in larger systems naturally leads to the next step: observability.</p><blockquote><p>Note: <a href="https://github.com/microsoft/agent-framework/issues/5806">Currently Telemetry is not supported within DevUI when using .NET.</a></p></blockquote><h2 id="introducing-ai-observability-across-tracing-debugging-cost-and-evaluation">Introducing AI Observability Across Tracing, Debugging, Cost, and Evaluation</h2><p>As workflows grow beyond simple development scenarios, observability becomes a critical part of the agent development loop. This is where the observability gap begins to appear. DevUI helps developers understand what happened during a local debugging session, but production systems require persistent visibility into how agents behave across users, sessions, models, tools, latency, cost, quality, and failures. While DevUI provides interactive debugging for local workflows, observability extends visibility across sessions, tool calls, evaluations, and distributed execution paths using OpenTelemetry and .NET&rsquo;s built-in <code>Activity</code> pipeline.</p><p>The .NET SDK instruments agents built with either <code>IChatClient</code> from <code>Microsoft.Extensions.AI</code> or <code>IAgent</code> from Microsoft Agent Framework. This allows developers to trace LLM requests, streaming responses, workflow execution, and tool calls while integrating naturally with existing .NET telemetry infrastructure.</p><p>To enable observability, we'll use <a href="https://www.telerik.com/ai-observability-platform">Progress AI Observability Platform</a>. The Progress AI Observability Platform is a cloud-based platform for tracing, debugging, cost analysis, and evaluation of AI applications. It provides visibility into how AI agents behave across models, tools, and sessions, helping teams identify issues as they happen, understand their impact, and continuously improve workflow quality over time.</p><p>Start by installing the .NET SDK for Progress AI Observability Platform.</p><pre><code class="language-bash">dotnet add package Progress.Observability.Instrumentation
</code></pre><p>Next, configure the desired options. Tags can be included for easier filtering within the observability reporting screen. In this example, the Environment name will capture: Development, Staging, and Production tags. Once the options are declared, the tracer is initialized.</p><pre><code class="language-csharp">// Configure the observiablity options, tags, and keys
var observabilityOptions = new ObservabilityOptions()
{
    AppName = builder.Environment.ApplicationName,
    ApiKey = builder.Configuration["Progress:ObservabilityKey"]!,
    AdditionalTags = new List&lt;string&gt; { builder.Environment.EnvironmentName }
};

// Initialize the observability tracer
ObservabilityTracer.Initialize(observabilityOptions);
</code></pre><p>Start capturing traces at the top level of applications using <code>IChatClient</code>, observability is added directly to the client:</p><pre><code class="language-csharp">var chatClient = new ChatClient(
        "gpt-4o-mini",
        new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAI:Key")),
        new OpenAIClientOptions { Endpoint = azureOpenAIEndpoint })
    .AsIChatClient()
    .AddObservability(o =&gt; o = observabilityOptions);

</code></pre><p>Applications using Microsoft Agent Framework can initialize observability through <code>AddObservability()</code> during agent construction:</p><pre><code class="language-csharp">var chatClient = new ChatClient(
        "gpt-4o-mini",
        new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAI:Key")),
        new OpenAIClientOptions { Endpoint = azureOpenAIEndpoint })
    .AsIChatClient()
    .AddObservability(o =&gt; o = observabilityOptions);
</code></pre><p>With observability enabled, you'll run the application using DevUI. Exercise the agents and workflows as before, but this time each session is collected for a deeper analysis through the AI Observability Platform. From the dashboard, you can perform cost analysis, run evaluations, and drill into traces across multiple sessions.</p><p>From the main Observations tab, all sessions are shown for a given period. Through the tagging feature the data is easily categorized, seen in Figure 6.</p><p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/observability/observability-all.png?sfvrsn=c8dd45c6_2" height="366" style="max-width:100%;height:auto;" width="800" alt="" /></p><p><strong>Figure 6: </strong>The observability platform with the Observations tab selected. 1) The date range and filter selection is chosen. 2) All sessions within the filter criteria.</p><p>Clicking on an individual item displays a complete breakdown of the trace log, see Figure 7. Including status, latency, cost and more. A tree interface allows deeper debugging, showing individual agent calls within the workflow, along with their corresponding inputs and outputs.</p><p><img sf-image-responsive="true" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/.net-maui-aiprompt/observability-drill-down.png?sfvrsn=92ef854f_2" height="458" style="max-width:100%;height:auto;" width="800" alt="" /></p><p><strong>Figure 7: </strong>A trace is selected. 1) The status section shows top level telemetry data including status, latency and costs. 2) A tree interface expands into nested trace activity. 3) The inputs and outputs of the selected agent are displayed.</p><p>This approach creates a more complete agent development loop, allowing local experimentation in DevUI to evolve into production-ready diagnostics and monitoring. In this article, tracing serves as the primary example, however production AI observability extends much further. Teams also need debugging, cost analysis, evaluation, governance, and operational insight to successfully operate AI systems at scale.</p><p>Progress AI Observability Platform supports that broader production view, helping teams connect trace-level detail with the trust, scale, and operational control required for enterprise AI applications.</p><h2 id="next-steps">Next Steps</h2><p>DevUI provides a strong starting point for building and debugging agents locally, but production AI systems require deeper visibility across tracing, debugging, evaluation, and operational monitoring.</p><p>Explore the <a href="https://www.telerik.com/ai-observability-platform">Progress AI Observability Platform</a> to see how observability extends the agent development loop from local experimentation to production-ready diagnostics.</p><p>Have questions or want to share what you're building? Join the conversation with the team on <a href="https://discord.gg/tK6RuSWKf">Discord</a>.</p><img src="https://feeds.telerik.com/link/10827/17352185.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:58c27fcc-97ec-47bb-b8ed-a6bfff50ad27</id>
    <title type="text">Deploying Containerized NestJS Applications on GCP Using Cloud Run</title>
    <summary type="text">While there are numerous ways to deploy an app to Cloud Run, see how to containerize and deploy a NestJS API. We will use a few products on GCP, such as Buildpacks and Artifact Registry, to build and deploy our image, and then finally deploy it to Cloud Run.</summary>
    <published>2026-05-21T14:35:45Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Christian Nwamba </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17345725/deploying-containerized-nestjs-applications-gcp-using-cloud-run"/>
    <content type="text"><![CDATA[<p><span class="featured">While there are numerous ways to deploy an app to Cloud Run, see how to containerize and deploy a NestJS API. We will use a few products on GCP, such as Buildpacks and Artifact Registry, to build and deploy our image, and then finally deploy it to Cloud Run.</span></p><p><a target="_blank" href="https://cloud.google.com/run">Google Cloud Run</a> is a serverless platform that allows developers to deploy and scale a wide range of applications, from web, server-side and functions to AI/ML workloads. Internally, it runs all applications as containerized payloads.</p><p>While there are numerous ways to deploy an app to Cloud Run, in this article, we will see how to containerize and deploy a NestJS API. We will use a few products on GCP, such as <a target="_blank" href="https://docs.cloud.google.com/docs/buildpacks/overview">Buildpacks</a> and <a target="_blank" href="https://docs.cloud.google.com/artifact-registry/docs">Artifact Registry</a>, to build and deploy our image, and then finally deploy it to Cloud Run.</p><h2 id="prerequisites">Prerequisites</h2><p>To proceed with this guide, it is assumed you are comfortable with TypeScript and have basic knowledge of building a web server with the NestJS framework.</p><h2 id="setting-up-a-nestjs-project">Setting Up a NestJS Project</h2><p>Assuming you have the <a target="_blank" href="https://docs.nestjs.com/cli/overview#installation">NestJS CLI</a> installed, open your terminal and run the following command to set up a basic NestJS project:</p><pre class=" language-shell"><code class="prism  language-shell">nest new sample-project
</code></pre><p>Follow the prompt to set up the project in a folder called sample-project. Feel free to choose your preferred name. We will be making changes to our project as we proceed.</p><h2 id="setting-up-our-project-on-gcp">Setting Up Our Project on GCP</h2><p>Let&rsquo;s now set up a project on GCP. To achieve this, you can do one of the following:</p><ul><li>Create a project using the Firebase console UI or CLI</li><li>Directly create it on the GCP console UI or using the Google Cloud CLI</li></ul><p>Regardless of which method we choose, we will get the same result. However, in our case, we will be using the second option and will mostly be working with the Google Cloud CLI in our terminal to create resources.</p><p>Assuming you have the Google Cloud CLI installed, open your terminal and run the following commands to set up the CLI. Skip these steps if you have already configured it.</p><p>Start by logging in:</p><pre class=" language-shell"><code class="prism  language-shell">gcloud auth login
</code></pre><p>To verify the logged-in account, run this command: <code>gcloud auth list</code>.</p><p>Next, run the following command to create a project:</p><pre class=" language-shell"><code class="prism  language-shell">gcloud projects create dummy-nest-swish0062 --name dummy-nest-project --set-as-default
</code></pre><p>We specify the project ID and the project name, and set it as the default project in our CLI.</p><p>We will also need to enable billing on the project to be able to use Cloud Run. Run the following command:</p><pre class=" language-shell"><code class="prism  language-shell">gcloud billing accounts list 
</code></pre><p>The command above lists the available billing accounts, which will return a list that looks like so:</p><p><img title="List of billing account" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/list-of-billing-account.png?sfvrsn=7ca2978e_2" alt="List of billing account" /></p><p>Next, link the billing account by running this command:</p><pre class=" language-shell"><code class="prism  language-shell">gcloud billing projects link dummy-nest-swish0062 --billing-account=017F58-A35A34-XXXXXX
</code></pre><p>The command above is similar to clicking create project on GCP and filling the form as shown below:</p><p><img title="Creating a project on GCP console" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/creating-a-project-on-gcp-console.png?sfvrsn=197f9625_2" alt="Creating a project on GCP console" /></p><h2 id="google-cloud-run-knative-and-kubernetes">Google Cloud Run, Knative and Kubernetes</h2><p>Google Cloud Run is built on top of Knative, and Knative is built on top of Kubernetes. These tools are designed to simplify the deployment of containerized applications.</p><p><img title="Google Cloud Run, Knative, and Kubernetes" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/google-cloud-run-knative-kubernetes.png?sfvrsn=e3ea634_2" alt="Google Cloud Run, Knative, and Kubernetes" /></p><p>As we move from top to bottom, there is increased experience and expertise required, more control, more knowledge gaps to be filled, and, of course, a greater margin for error.</p><p>The opposite applies when moving from bottom to top, with Google Cloud Run at the apex. Cloud Run requires the least experience from developers and gives them the best deployment experience while doing all the heavy lifting.</p><p>To understand the benefits of Cloud Run, let&rsquo;s do a basic walkthrough from bottom to top, outlining the struggles and benefits at each level.</p><h3 id="containers-and-kubernetes">Containers and Kubernetes</h3><p>It all starts with having a project written in some language that needs to be deployed live to users. To proceed, the developer may need to understand how to use a container runtime like Docker or Podman to build images. Next, they need to put the images on some registry (e.g., Docker Hub).</p><p>To deploy the images, that is, run them as containers in production, a lot of things can go wrong. They need to determine how many instances of the container to run, which ports to expose, how to route traffic, and much more. To achieve this, learning how to use a tool like Kubernetes might help, since it allows the developer to define the desired state of the application. The developer needs to understand how Kubernetes works, understanding concepts like the control plane, data plane, workloads, pods, deployments, services and ingresses.</p><h3 id="knative">Knative</h3><p>To save developers the stress, Knative was built. It abstracts all the inner complexities of working directly with Kubernetes and makes it easy to build and scale serverless applications with zero knowledge of containers, Kubernetes or any of its concepts. Since it is based on Kubernetes, it is highly portable and can run on any cloud platform.</p><p>Knative consists of three main components:</p><ol><li><p><strong>Functions Framework:</strong> A framework that allows developers to write HTTP-triggerable serverless functions in their preferred programming language. As of the time of writing, four languages are supported (Go, Python, Java and TypeScript/JavaScript). After writing their functions, developers can test them locally. The Functions Framework handles containerizing the function code, storing it in a registry and then passing it to Knative Serving for deployment.</p></li><li><p><strong>Knative Serving:</strong> This is responsible for deploying and running containers on top of Kubernetes. Containers can hold the logic for any HTTP-triggerable workload&mdash;for example, one using the Functions Framework, our NestJS web server, or an AI/ML workload. Under the hood, it interacts with Kubernetes to create service definitions, which house all the configurations required to run the containers, route incoming traffic to them and handle scaling as well. For AI/ML workloads, it verifies containers have access to GPUs. Service definitions also maintain revisions of the service, which are snapshots or versions of the configurations and the state of our application, allowing developers to roll back to previous states in case of errors or failures.</p></li><li><p><strong>Knative Eventing:</strong> Provides APIs for developers to employ an event-driven architecture suitable for building loosely coupled services. These APIs allow developers to route events between services via HTTP. Events are usually represented in the form of JSON. They originate from an event source and are then moved to a message broker, which routes the payload to an event consumer. An event source or consumer could be services running on Knative Serving or Kubernetes; event sources may also be external services and systems, such as databases.</p></li></ol><h3 id="cloud-run">Cloud Run</h3><p>Finally, we have Cloud Run, which, in the simplest terms, is Knative for Google Cloud Platform with extra superpowers:</p><ul><li>Deployment directly from source code or using containers, with or without knowledge of container runtimes like Docker or Podman</li><li>Runs containers in an isolated environment where running containers can still access your other cloud resources</li><li>Multiple options to run containers: as services, jobs or worker pools</li><li>Functions Framework with support for more languages, allowing you to write and deploy application logic in minutes</li><li>Flexible payment options&mdash;either pay per request or pay per instance&mdash;with autoscaling handled automatically</li><li>CI/CD tools to automatically deploy new versions of your application to production</li></ul><h2 id="deployment-options">Deployment Options</h2><p>Regardless of the method we choose to deploy an application, we already know that it ends up running as a container. Generally, we can deploy our application as one of the following:</p><ul><li><strong>Service:</strong> Used for HTTP-triggerable resources.</li><li><strong>Job:</strong> Jobs are typically used when we want our code to perform expensive computations that we trigger manually. Jobs are not called via HTTP.</li><li><strong>Worker pool:</strong> These are used to run resources that serve as consumers for jobs managed by a broker or queue service. Worker pools are always running and constantly listening for new data to process.</li></ul><p>Since our NestJS application is HTTP-triggerable and listens on a port, we will be deploying it as a service.</p><p>When deploying our application as a service, we have two main options:</p><ul><li><strong>Deploy from source:</strong> Here, you simply point Cloud Run to a repository (e.g., on GitHub), and it takes care of containerizing the application and deploying it. This option is available only for services.</li><li><strong>Deploy from container:</strong> Here, you point it to a container (e.g., on Docker Hub or Artifact Registry), and then it takes care of the rest. This is available for all resource types.</li></ul><p>We will be using the second option, since this is the goal of this article.</p><h2 id="deploy-from-container">Deploy from Container</h2><p>When deploying from containers, we can choose to build and publish the image either locally or remotely:</p><ul><li><strong>Locally:</strong> Here we install Docker, Podman or some other container runtime on a PC or VM, then build an image. To build the image locally, we can either write our build configuration in a Dockerfile or skip using a Dockerfile completely and install a local buildpack like the Pack CLI to build the image. We then publish it on Docker Hub, retrieve the URL to that image, and feed it to the Cloud Run console to deploy our app.</li><li><strong>Remotely:</strong> This is the option we will be using. Here, we don&rsquo;t need to install any tools. We will leverage Google Cloud Buildpacks to build the image remotely, then proceed to push the built image to Google&rsquo;s Artifact Registry and use the URL to the image to deploy the service.</li></ul><h2 id="preparing-our-nestjs-project-before-deployment">Preparing Our NestJS Project Before Deployment</h2><p>We will also need to make a few changes to our NestJS project before we can proceed with deployment.</p><h3 id="defining-the-project-descriptor">Defining the Project Descriptor</h3><p>In the root of your NestJS project, open your terminal and run the following command:</p><pre class=" language-shell"><code class="prism  language-shell">touch project.toml
</code></pre><p>Update its contents to match the following</p><pre class=" language-js"><code class="prism  language-js"><span class="token punctuation">[</span><span class="token punctuation">[</span>build<span class="token punctuation">.</span>env<span class="token punctuation">]</span><span class="token punctuation">]</span>
name <span class="token operator">=</span> <span class="token string">"GOOGLE_ENTRYPOINT"</span>
value <span class="token operator">=</span> <span class="token string">"node dist/main"</span>
</code></pre><p>Earlier, we said we will be using Google&rsquo;s Cloud Build service and the buildpacks it provides to build our application&rsquo;s image. A project descriptor is a file used to guide the repository (i.e., the Cloud Build service) on how to build the image. Think of it as informing the Google Cloud Build service on how to update the contents of the Dockerfile before it builds the image and when it runs the image as a container.</p><p>There are special environment variable names, some of which are specific to Google Cloud Build (i.e., for any runtime such as Node or Java) and others specific to our project&rsquo;s runtime (i.e., Node.js in our case).</p><p>In the <code>project.toml</code> file, we specified the one called <code>GOOGLE_ENTRYPOINT</code>. Without this variable, Cloud Run will not know how to execute our container. The value of this variable is synonymous with the CMD block in a Dockerfile.</p><p>It is important to note that the environment variables in the <code>project.toml</code> file are only for building the image and running the container. Later, we will describe how to add runtime-specific environment variables like database secrets that will be used in our NestJS app.</p><h2 id="configuring-the-port">Configuring the Port</h2><p>Update the <code>main.ts</code> file to look like this:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> NestFactory <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/core'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AppModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.module'</span><span class="token punctuation">;</span>

<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">bootstrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> app <span class="token operator">=</span> <span class="token keyword">await</span> NestFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>AppModule<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">await</span> app<span class="token punctuation">.</span><span class="token function">listen</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span>PORT <span class="token operator">||</span> <span class="token number">3000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">bootstrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>The PORT environment variable is a special reserved variable that will be injected by Cloud Run when it runs our NestJS backend in a container. We updated the app to listen to the value of the PORT variable or fall back to the default value of 3000.</p><h2 id="building-and-deploying-our-nestjs-app-as-a-service">Building and Deploying Our NestJS App as a Service</h2><p>In this section, we will be doing the following:</p><ul><li>Creating an artifact repository</li><li>Building and publishing the image to the artifact repository</li><li>Creating a service on Cloud Run using the image URL</li></ul><h2 id="creating-an-artifact-repository">Creating an Artifact Repository</h2><p>Open your terminal, and run the following command to create a repository:</p><pre class=" language-shell"><code class="prism  language-shell">gcloud artifacts repositories create dummy-nest-repo\
    --repository-format=docker \
    --location=europe-west2 \
    --description=" this "repo will hold my nestjs app" \
    --immutable-tags 
</code></pre><p>A repository holds one or more images. In the command above, we created one called &ldquo;dummy-nest-repo&rdquo; in the europe-west2 region with the Docker repository format.<br />If the command executes successfully, we get the newly created repository.</p><p>Here is a snapshot of the GCP console UI showing the newly created repository:</p><p><img title="Newly created repo on GCP console" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/newly-created-repo-on-gcp-console.png?sfvrsn=a38ed974_2" alt="Newly created repo on GCP console" /></p><h2 id="building-and-publishing-the-image-to-the-artifact-repository">Building and Publishing the Image to the Artifact Repository</h2><p>With our repository available, run the following command to trigger a build and publish it to the repository:</p><pre class=" language-shell"><code class="prism  language-shell">gcloud builds submit --pack image=europe-west2-docker.pkg.dev/dummy-nest-swish0062/dummy-nest-repo/dummy-app:0.0001 
</code></pre><p>The command above triggers the Cloud Build service to build and deploy our image to the dummy-nest-repo repository.</p><p>The image URL is a string that takes the following form:</p><pre class=" language-js"><code class="prism  language-js">REGION<span class="token operator">-</span>docker<span class="token punctuation">.</span>pkg<span class="token punctuation">.</span>dev<span class="token operator">/</span>PROJECT<span class="token operator">-</span>ID<span class="token operator">/</span>REPOSITORY_NAME<span class="token operator">/</span>IMAGE_NAME<span class="token punctuation">:</span>TAG
</code></pre><p><img title="Image built and uploaded in repo" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/image-built-and-uploaded.png?sfvrsn=22dfb8ed_2" alt="Image built and uploaded in repo" /></p><h2 id="creating-a-service-on-cloud-run-using-the-image-url">Creating a Service on Cloud Run Using the Image URL</h2><p>Open your terminal and run the following command to deploy the image as a service:</p><pre class=" language-shell"><code class="prism  language-shell">gcloud run deploy my-nestjs-app  --image europe-west2-docker.pkg.dev/dummy-nest-swish0062/dummy-nest-repo/dummy-app:0.0001
</code></pre><p>The command above deploys a service called &ldquo;my-nestjs-app&rdquo; using the URL to the image we just uploaded.</p><p>The screenshot below shows the result of running the command.</p><p><img title="Deploying our NestJS application as a service on google cloud run using the image URL" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/deploying-our-nestjs-application.png?sfvrsn=e21c81ce_2" alt="Deploying our NestJS application as a service on google cloud run using the image URL" /></p><p>As seen above, we get a URL which we can use to connect to the application. We can visit this endpoint in our browser and receive a &ldquo;Hello World&rdquo; response from our NestJS server, as shown below.</p><p><img title="Testing the service URL in the browser" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/testing-the-service-url.png?sfvrsn=e7223305_2" alt="Testing the service URL in the browser" /></p><p>Also, on the GCP console, we can see the service, its revisions, configurations, routing, etc., similar to the definitions of the Knative Serving component described earlier.</p><p><img title="GCP console" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/gcp-console.png?sfvrsn=d91513eb_2" alt="GCP console" /></p><h2 id="adding-runtime-specific-environment-variables">Adding Runtime-Specific Environment Variables</h2><p>Server-side applications may use one or more environment variables to connect to a database, communicate with third-party APIs, etc. In this section, we will see how to include runtime environment variables when deploying services. We will be doing the following:</p><ul><li>Adding .env files to our NestJS app</li><li>Setting up and configuring the Config Module</li><li>Deploying a new version of the service referencing the environment variable files</li></ul><h3 id="adding-.env-files-to-our-nestjs-app">Adding .env files to our NestJS app</h3><p>Assuming you are in the project&rsquo;s root, open your terminal and run the following command to create two files: <code>.env.dev</code> and <code>.env.prod</code>.</p><pre class=" language-shell"><code class="prism  language-shell">touch .env.dev prod.env
</code></pre><p>These files will hold environment variables for development and production, respectively. Let&rsquo;s proceed to update their contents.</p><p>Update the <code>.env.dev</code> file with the following:</p><pre class=" language-js"><code class="prism  language-js">MESSAGE<span class="token operator">=</span><span class="token string">"this is dev message"</span>
</code></pre><p>Update the <code>prod.env</code> file with the following:</p><pre class=" language-js"><code class="prism  language-js">MESSAGE<span class="token operator">=</span><span class="token string">"this is production message"</span>
</code></pre><p>We included a variable named <code>MESSAGE</code> in each file, which holds a dummy message.</p><h3 id="setting-up-and-configuring-the-config-module">Setting Up and Configuring the Config Module</h3><p>Usually, when working with environment variables in a NestJS application, the <code>ConfigModule</code> is the recommended way to use and access them. That is what we will be doing in this guide.</p><p>Let&rsquo;s now install it in our app.</p><pre class=" language-shell"><code class="prism  language-shell">pnpm install -save @nestjs/config
</code></pre><p>Next, let&rsquo;s update the <code>app.module.ts</code> file to use this module:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AppController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.controller'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AppService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
    imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>

    ConfigModule<span class="token punctuation">.</span><span class="token function">forRoot</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
        isGlobal<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
        ignoreEnvFile<span class="token punctuation">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span>NODE_ENV <span class="token operator">===</span> <span class="token string">'production'</span><span class="token punctuation">,</span>
        envFilePath<span class="token punctuation">:</span> <span class="token punctuation">[</span>
        <span class="token string">'.env.dev'</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">]</span><span class="token punctuation">,</span>
    controllers<span class="token punctuation">:</span> <span class="token punctuation">[</span>AppController<span class="token punctuation">]</span><span class="token punctuation">,</span>
    providers<span class="token punctuation">:</span> <span class="token punctuation">[</span>AppService<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>
</code></pre><p>The <code>envFilePath</code> points to files where we want to load environment variables from. We only specified <code>.env.dev</code> since we will be using that during development. However, in production, we ignore all <code>.env</code> files since they will be injected for us automatically in our container.</p><p>Note that the <code>process.env.NODE_ENV</code> variable will be automatically injected by the Cloud Build service when building the image and will default to production. We can override this value in the <code>project.toml</code> file in case we want different behavior in different environments.</p><p>Let&rsquo;s now update the <code>app.service.ts</code> file and update its <code>getHello</code> method:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppService</span> <span class="token punctuation">{</span>
    <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> readonly configService<span class="token punctuation">:</span> ConfigService<span class="token punctuation">)</span> <span class="token punctuation">{</span>

    <span class="token punctuation">}</span>
    <span class="token function">getHello</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>configService<span class="token punctuation">.</span>getOrThrow<span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token string">'MESSAGE'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The <code>getHello</code> method is now updated to return the contents of the <code>MESSAGE</code> environment variable.</p><p>Start the application locally by running:</p><pre class=" language-shell"><code class="prism  language-shell">pnpm run start:dev
</code></pre><p>If we visit localhost:3000 in our browser, we see the contents of the message from the <code>.env.dev</code> variable, as shown below:</p><p><img src="https://www.telerik.com/sfimages/default-source/blogs/2026/2026-05/message-from-env-dev-variable.png" alt="Message from the .env.dev variable" title="Message from the .env.dev variable" /></p><p>Let&rsquo;s now proceed to build and deploy a new version of our service.</p><pre class=" language-shell"><code class="prism  language-shell">gcloud builds submit --pack image=europe-west2-docker.pkg.dev/dummy-nest-swish0062/dummy-nest-repo/dummy-app:0.0002
</code></pre><p>The only notable change in the build command is that the new image has a tag of 0002. Next, let&rsquo;s deploy it.</p><pre class=" language-shell"><code class="prism  language-shell">gcloud run deploy my-nestjs-app  --image europe-west2-docker.pkg.dev/dummy-nest-swish0062/dummy-nest-repo/dummy-app:0.0002 --env-vars-file prod.env 
</code></pre><p>Notice we included the <code>&ndash;env-vars-file</code> option and set it to the <code>prod.env</code>.</p><blockquote><p>As a side note, verify that the file extension of the file holding environment variables that you intend to ship to production ends with <code>.env</code> (e.g., prod.env in our case). Setting it to <code>.env.prod</code> will not work.</p></blockquote><p>On the GCP console, look at our service and click on <strong>Edit and deploy new revision</strong>.<br />In the <strong>Variables and Secrets</strong> tab under <strong>Edit Container</strong>, we see the newly added variable, as shown below:</p><p><img title="Viewing runtime environment variables for servicee" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/viewing-runtime-environment-variables.png?sfvrsn=19e97179_2" alt="Viewing runtime environment variables for service" /></p><p>When we visit our service URL, we get the new message, as shown below:</p><p><img title="Production service url outputs contents of the injected runtime environment variable" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/production-service-url-output.png?sfvrsn=55f065b_2" alt="Production service url outputs contents of the injected runtime environment variable" /></p><h2 id="best-practices">Best Practices</h2><p>In this section, we will discuss a few best practices to keep in mind when deploying containerized applications on Google Cloud Run. We have already established the fact that when deploying services, we can have billing set either per request (default) or per instance. We will go over some general guidelines for all services to be deployed on Cloud Run and runtime-specific guidelines for Node.js.</p><ol><li>If services are configured to run as request-based, they should not perform background tasks since their runtime is short-lived (i.e., limited to only when there are incoming requests). If you need services to perform background tasks, run them as instance-based and set at least one running instance.</li><li>Start containers quickly. This can be done by keeping containers lightweight and removing unnecessary dependencies in your code. Also, if you are building the image yourself, use stable and community-maintained base images.</li><li>For Node.js applications, minimize the number of dependencies. When using many dependencies, you should employ lazy loading to only load them when necessary to minimize startup time. Preferably, start your containers using <code>node</code> instead of <code>npm</code>. For example, in our case we started our container using <code>node dist/main</code> instead of <code>npm run start</code>, which is slower.</li></ol><h2 id="next-steps">Next Steps</h2><p>When it comes to deploying applications, there is no one-size-fits-all approach. We have covered one of the numerous ways to deploy our application on GCP using the Cloud Run service. However, we can still make some improvements in our approach. The obvious one is the fact that we have to manually type each command.</p><p>Also, to streamline our workflow, we need to enable CI/CD to provide a better experience when deploying our code. We can use GitHub Actions and any of the Google Cloud Run provided workflows to automate that process.</p><p><img title="Google Cloud Run workflows" src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-05/google-cloud-run-workflows.png?sfvrsn=1024a270_2" alt="Google Cloud Run workflows" /></p><h2 id="conclusion">Conclusion</h2><p>While there are numerous options for deploying applications, in this guide we focused on using Google Cloud and the Cloud Run service. Hopefully, this will serve as a potential option for deploying your applications in future projects.</p><hr /><p><strong>Read more:</strong> <a href="https://www.telerik.com/blogs/build-nestjs-ai-chatbot-google-gemini" target="_blank">How to Build a NestJS AI Chatbot with Google Gemini</a></p><img src="https://feeds.telerik.com/link/10827/17345725.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:ce866956-4550-4c69-a546-7ae35e4fdbc6</id>
    <title type="text">Creating Reliable AI Resources with Telerik Agent Tools API</title>
    <summary type="text">The RAG resources you create to use with your LLMs are strategic resources, just like your organization’s databases. Telerik Agent Tools let you create the custom tools for managing those resources.</summary>
    <published>2026-05-20T13:36:52Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Peter Vogel </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17344837/creating-reliable-ai-resources-telerik-agent-tools-api"/>
    <content type="text"><![CDATA[<p><span class="featured">The RAG resources you create to use with your LLMs are strategic resources, just like your organization&rsquo;s databases. Telerik Agent Tools let you create the custom tools for managing those resources.</span></p><p>Like your organization&rsquo;s databases, you should regard your AI resources as strategic, <em>organizational</em> resources. And, like the data in your organization&rsquo;s databases, you need to manage the content used by your AI resources to enable those resources give you reliable, grounded answers. Telerik Agent Tools API lets gives you the tools for automating the process of creating and maintaining the content in your AI resources.</p><p>In an earlier post, I showed how to use Progress Telerik <a target="_blank" href="https://www.telerik.com/document-processing-libraries">Document Processing Libraries</a> (DPL) to create an <a target="_blank" href="https://www.telerik.com/blogs/creating-custom-ai-agent-telerik-tools-1-configuring-llm-azure-ollama">AI resource with content from existing documents and then tie that resource to a Large Language Model</a> (LLM) that your users could query.</p><p>Tying an LLM to your content creates a <a target="_blank" href="https://www.telerik.com/blogs/understanding-rag-retrieval-augmented-generation">Resource Augmented Generation</a> (RAG) resource that helps get your users grounded answers driven by the content you load into your resource. And using the DPL toolset is a great solution when you&rsquo;re building an application that creates a RAG resource &ldquo;as needed.&rdquo;</p><p>But if you want to reliably create/update a RAG resource that will be used (and reused) by multiple applications and multiple users, then you need the Telerik Agent Tools API. Once you have a reliable process for creating your RAG resource, the Agent Tools API also lets you then use that resource to support your users by integrating with Microsoft&rsquo;s latest toolset for responding to your user&rsquo;s prompts.</p><p>In this post, I&rsquo;m going to cover how to construct an automated workflow for creating a RAG resource that can be used across applications and users. In my next post, I&rsquo;ll cover how to use that resource with the LLM of your choice.</p><h2 id="building-a-rag-resource-workflow">Building a RAG Resource Workflow</h2><p>You can use the Tools API to create an interactive application that lets you manage the documents that make up your resource&rsquo;s content. However, for this post, I&rsquo;m going to assume a simpler process than that (though I&rsquo;ll cover all the tools you&rsquo;d need for an interactive solution).</p><p>For this post, I&rsquo;m going to assume there&rsquo;s a folder that holds all the documents with the content that should be in my RAG resource (which might be Word documents, spreadsheets, PDF, text files&mdash;basically, all the content that you can load with Telerik DPL tools). Users control what goes into my resource by adding and removing documents from that folder.</p><p>In this case study, then, I build my RAG resource by running a batch program that reads all the documents in the folder, adds them all to a RAG resource in memory, and then saves the new resource, replacing any existing version with a new, updated version.</p><h2 id="configuring-your-project">Configuring Your Project</h2><p>To start building an application to manage your RAG resource, you must first add the Telerik.Documents.AI.Tools.Core NuGet package to your project. After that, you need the specific Telerik packages that support the document types that you&rsquo;ll add to your RAG resource.</p><p>For my case study, I&rsquo;m just going to use PDF documents, so I just need the &ldquo;Fixed&rdquo; packages:</p><pre><code>- Telerik.Documents.AI.Tools.Fixed.Core
- Telerik.Documents.AI.AgentTools.Fixed 
</code></pre><p>If you&rsquo;re working with spreadsheets, you&rsquo;ll want to add the equivalent Spreadsheet packages:<br />- Telerik.Documents.AI.Tools.Spreadsheet.Core<br />-Telerik.Documents.AI.AgentTools.Spreadsheet)</p><p>To support Microsoft Word, HTML and text documents, you also need to add the Telerik.Documents.AI.Tools.Flow.Core package.</p><p>And, of course, there&rsquo;s no reason you couldn&rsquo;t make your life simpler by adding all the <a target="_blank" href="http://Telerik.Documents.AI">Telerik.Documents.AI</a>.* libraries to your application, even if you don&rsquo;t need to use them all right now. In this case study, I also didn&rsquo;t take advantage of the some of the other packages available&mdash;the Telerik.Documents.AI.AgentTools.Conversion package that handles conversions between document types, for example.</p><h2 id="building-your-rag-resource">Building Your RAG Resource</h2><p>The process for creating a RAG resource begins with creating an in-memory repository which holds your documents. A repository can hold one of three categories of documents: spreadsheets, PDF documents or Word and Word-related documents.</p><p>In my case study, I&rsquo;m only going to work with PDF documents (which Progress Telerik classifies as &ldquo;Fixed&rdquo; documents), so I created an <code>inMemoryFixedDocumentRepository</code> object and then stored a reference to that repository in an <code>IFixedDocumentRepository</code> variable.</p><p>The code to do that looks like this:</p><pre class=" language-csharp"><code class="prism  language-csharp">IFixedDocumentRepository pdfRepo <span class="token operator">=</span> 
          <span class="token keyword">new</span> <span class="token class-name">InMemoryFixedDocumentRepository</span><span class="token punctuation">(</span> <span class="token keyword">new</span> <span class="token class-name">TimeSpan</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>All of the three types of repositories look very much alike though (they all share a common <code>IDocumentRepository</code> interface, for example). As a result, working with other document types is similar.</p><p>If, for example, you were loading spreadsheet files (&ldquo;Workbook&rdquo; documents), you&rsquo;d use the <code>InMemoryWorkbookRepository</code> object and the <code>IWorkbookRepository</code> interface. For Word and Word-related documents (&ldquo;Flow&rdquo; documents), you&rsquo;d use the <code>InMemoryFlowDocumentRepository</code> object and the <code>IFlowDocumentRepository</code> interface.</p><p>Repositories have some built-in functionality (they all include a <code>ListDocuments</code> method that returns a list of documents in the repository, for example). You will, however, want to extend your repository by attaching one or more toolsets containing tools that add additional functionality to your repository.</p><p>For my case study, I just needed to be able to import documents into an empty repository so the only toolset I attached was the <code>FixedFileManagementAgentTools</code> toolset that supports working with a PDF repository and has import (and export) methods for individual documents. To that toolset looks like this, I pass a reference to my repository, I pass a reference to my repository and the path to the folder containing the documents that the tools will work with:</p><pre class=" language-csharp"><code class="prism  language-csharp">FixedFileManagementAgentTools pdfFileTools <span class="token operator">=</span> 
            <span class="token keyword">new</span> <span class="token class-name">FixedFileManagementAgentTools</span><span class="token punctuation">(</span>pdfRepo<span class="token punctuation">,</span> repoFolderPath<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>You can add additional toolsets if you need additional functionality or need to work with more folders. If, for example, I wanted to work with PDF forms, I could add the <code>FixedDocumentFormAgentTools</code> toolset to my repository. When creating a toolset, you&rsquo;ll always need to pass a reference to the repository the toolset will be attached to, but other parameters (if any) will vary from one toolset to another.</p><h2 id="managing-repositories-with-registries">Managing Repositories with Registries</h2><p>If you&rsquo;re going to be working with multiple repositories in your workflow, you might want to create a registry to simplify managing your repositories. In my case study, for example, where I&rsquo;m potentially loading several different kinds of files, I could end up supporting all three types of repositories (PDF files, spreadsheets, Word and Word-related documents)&mdash;creating a registry simplifies switching between the different types of repositories.</p><p>Registries are created using the <code>DocumentRepositoryRegistry</code> object and support registering one repository of each document category using their <code>RegisterRespository</code> method.</p><p>You can then retrieve the repository you want from a registry by using the registry&rsquo;s <code>TryGetRepository</code> method, passing two parameters: the document type of the repository you want and an <code>out</code> parameter of type <code>IDocumentRepository</code>. Like other TryGet* methods, the <code>TryGetRepository</code> method returns return true if the method finds the repository you want and false if the method does not (the actual repository, if found, is loaded into the method&rsquo;s second, <code>out</code> parameter).</p><p>The following code creates a registry, registers my PDF repository using the <code>RegisterRepository</code> method and then immediately retrieves the repository using the registry&rsquo;s <code>TryGetRepository</code> method. Because the repository is returned using the common <code>IDocumentRepository</code> interface type, I cast the returned reference to the <code>IFixedDocumentRepository</code> type before working with it:</p><pre class=" language-csharp"><code class="prism  language-csharp">DocumentRepositoryRegistry registry <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

registry<span class="token punctuation">.</span><span class="token function">RegisterRepository</span><span class="token punctuation">(</span>DocumentType<span class="token punctuation">.</span>FixedDocument<span class="token punctuation">,</span> pdfRepo<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">if</span> <span class="token punctuation">(</span>registry<span class="token punctuation">.</span><span class="token function">TryGetRepository</span><span class="token punctuation">(</span>DocumentType<span class="token punctuation">.</span>FixedDocument<span class="token punctuation">,</span> 
                                                           <span class="token keyword">out</span> IDocumentRepository<span class="token operator">?</span> repo<span class="token punctuation">)</span> <span class="token punctuation">)</span>
<span class="token punctuation">{</span>
     IFixedDocumentRepository fixedRepo <span class="token operator">=</span> <span class="token punctuation">(</span>IFixedDocumentRepository<span class="token punctuation">)</span> repo<span class="token punctuation">;</span>
    <span class="token comment">//&hellip;work with the fixedRepo repository</span>
<span class="token punctuation">}</span>
</code></pre><p>You&rsquo;ll also want to create a registry if, as part of your workflow, you want to support converting documents between formats or merging multiple documents. The toolsets that support that are <code>ConvertDocumentsAgentTool</code> and <code>MergeDocumentsAgentTool</code> toolsets and they attach to registries rather than repositories.</p><h2 id="loading-saving-and-retrieving-your-repository">Loading, Saving and Retrieving Your Repository</h2><p>Adding a document to a repository is easy: Just call the appropriate import method on the appropriate toolset that&rsquo;s tied to the repository you want to update.</p><p>To add a PDF document to my repository, for example, I&rsquo;d use the <code>ImportFixedDocument</code> method on my <code>FixedFileManagementAgentTools</code> toolset, tied to my PDF repository. To use the <code>ImportFixedDocument</code> method, I pass the path to the document I want to load and its document format (e.g., PDF, XLSX, etc.). Optionally, I can pass a name for the document which will be returned as part of the <code>ListDocuments</code> method.</p><p>Which means that importing a PDF document into my repository would look like this:</p><pre class=" language-csharp"><code class="prism  language-csharp">CallToolResponse res <span class="token operator">=</span>
          pdfRepoTools<span class="token punctuation">.</span><span class="token function">ImportFixedDocument</span><span class="token punctuation">(</span>documentPath<span class="token punctuation">,</span> 
                                                                                         DocumentFormat<span class="token punctuation">.</span>PDF<span class="token punctuation">,</span>
                                                                                          Path<span class="token punctuation">.</span><span class="token function">GetFileNameWithoutExtension</span><span class="token punctuation">(</span>documentPath<span class="token punctuation">)</span>
                                                                                        <span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>The <code>CallToolResponse</code> object&rsquo;s <code>IsError</code> property will be set to <code>true</code> if your import succeeds (and the <code>Message</code> property will contain additional information regardless of whether the call succeeds or fails).</p><p>Once I&rsquo;ve added all the documents from my folder to my repository, I can save my repository with all of its content to a single file using my repository&rsquo;s <code>MergeAndExport</code> method. The <code>MergeAndExport</code> method requires three parameters:</p><ul><li>An array of the <code>id</code> property for each of the documents in the repository. You can use that array both to control which documents are exported and the merge order of the documents. I didn&rsquo;t bother and just used a LINQ <code>Select</code> method to retrieve all the <code>id</code> property values.</li><li>A <code>FileStream</code> object that points to your repository file (I set this up to overwrite my repository every time).</li><li>The document type for the file (<code>PDF</code>, in my case).</li></ul><p>You must close your <code>FileStream</code> after you finish exporting your documents. As a result, the code to export my repository of PDF documents would look like this:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span> ids <span class="token operator">=</span> pdfRepo<span class="token punctuation">.</span><span class="token function">ListDocuments</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Select</span><span class="token punctuation">(</span> doc <span class="token operator">=</span><span class="token operator">&gt;</span> doc<span class="token punctuation">.</span>Id <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
FileStream fs <span class="token operator">=</span>  <span class="token keyword">new</span> <span class="token class-name">FileStream</span><span class="token punctuation">(</span>repoFilePath<span class="token punctuation">,</span> FileMode<span class="token punctuation">.</span>CreateNew<span class="token punctuation">)</span><span class="token punctuation">,</span>
                                 
repo<span class="token punctuation">.</span><span class="token function">MergeAndExport</span><span class="token punctuation">(</span>ids<span class="token punctuation">,</span>
                                                fs<span class="token punctuation">,</span>  
                                                DocumentFormat<span class="token punctuation">.</span>PDF<span class="token punctuation">)</span><span class="token punctuation">;</span>
fs<span class="token punctuation">.</span><span class="token function">Close</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
</code></pre><p>You can now tie that file to an LLM using Microsoft <code>ChatClientAgent</code> object to let your users query your RAG resource. I&rsquo;ll walk through how to do that in my next post.</p><aside><hr data-sf-ec-immutable="" /><div class="row"><div class="col-4 u-normal-full u-small-mb0"><h4 class="u-fs20 u-fw5 u-lh125 u-mb0">Get Access to These Tools and More</h4></div><div class="col-8"><p class="u-fs16 u-mb0">The free 30-day trial of <a target="_blank" href="https://www.telerik.com/devcraft">Telerik DevCraft</a> lets you really kick the tires for yourself. <a target="_blank" href="https://www.telerik.com/try/devcraft-ultimate">Try it today!</a></p></div></div></aside><img src="https://feeds.telerik.com/link/10827/17344837.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:c37f48f7-cf9d-4e8d-95b3-010546859051</id>
    <title type="text">Protect Your Entities with Domain Validation</title>
    <summary type="text">Learn about error-prone domain validations in ASP.NET Core and how to correctly model them.</summary>
    <published>2026-05-19T13:32:48Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Assis Zang </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17344160/protect-entities-domain-validation"/>
    <content type="text"><![CDATA[<p><span class="featured">Learn about error-prone domain validations in ASP.NET Core and how to correctly model them.</span></p><p>In ASP.NET Core applications, developers often underestimate the importance of validating objects and classes. An <code>if</code> statement in the controller, a <code>FluentValidation</code> in the request or some <code>DataAnnotations</code> in the model, and that&rsquo;s it. The problem is that, if care isn&rsquo;t taken when implementing domain validations, responsibility ends up leaking, which can compromise the entire system&rsquo;s evolution.</p><p>In this article, we will analyze common examples of domain validation that are highly prone to errors and how these validations should be correctly modeled according to the principles of Domain-Driven Design (DDD).</p><h2 id="poorly-structured-validations">Poorly Structured Validations</h2><h3 id="validation-in-the-controller">1. Validation in the Controller</h3><p>Although it speeds up development, implementing validations in API controllers is discouraged, as it makes the controller highly coupled to business rules. Furthermore, the rules created there are impossible to reuse. Finally, validations in controllers allow other parts of the code to execute the same actions, bypassing the rules.</p><p>Consider the following example:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span><span class="token function">Route</span><span class="token punctuation">(</span><span class="token string">"api/[controller]"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token punctuation">[</span>ApiController<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">OrderController</span> <span class="token punctuation">:</span> ControllerBase
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> OrderRepository _orderRepository<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">OrderController</span><span class="token punctuation">(</span>OrderRepository orderRepository<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _orderRepository <span class="token operator">=</span> orderRepository<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span>HttpPost<span class="token punctuation">]</span>
    <span class="token keyword">public</span> IActionResult <span class="token function">Create</span><span class="token punctuation">(</span>CreateOrderDto dto<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>dto<span class="token punctuation">.</span>Total <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
            <span class="token keyword">return</span> <span class="token function">BadRequest</span><span class="token punctuation">(</span><span class="token string">"Total must be greater than zero"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">if</span> <span class="token punctuation">(</span>dto<span class="token punctuation">.</span>Items <span class="token operator">==</span> <span class="token keyword">null</span> <span class="token operator">||</span> <span class="token operator">!</span>dto<span class="token punctuation">.</span>Items<span class="token punctuation">.</span><span class="token function">Any</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token keyword">return</span> <span class="token function">BadRequest</span><span class="token punctuation">(</span><span class="token string">"Order must have at least one item"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">var</span> order <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Order</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            Total <span class="token operator">=</span> dto<span class="token punctuation">.</span>Total<span class="token punctuation">,</span>
            Items <span class="token operator">=</span> dto<span class="token punctuation">.</span>Items
        <span class="token punctuation">}</span><span class="token punctuation">;</span>

        _orderRepository<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">return</span> <span class="token function">Ok</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The code above is a common example of validations in the Controller, and it&rsquo;s a bad example because it has all the flaws mentioned earlier. Note that the <code>_orderRepository.Add(order);</code> method is called at the end, meaning there are loopholes here.</p><p>Imagine that the parameter <code>CreateOrderDto dto</code> has a total greater than zero, and the Items list has one item, but the rest of the properties are null. Even so, the <code>Add</code> method will be called and will create problematic entities or generate a bug.</p><h3 id="validation-in-the-service-class">2. Validation in the Service Class</h3><p>Although common, the use of validations in the Service class should also be avoided because they place business rules in the wrong place. Considering the previous example, they only changed location but still remain a problem.</p><p>When a business rule is coupled to a Service, the domain model becomes passive, and entities can be created or modified in invalid states because there is nothing to prevent them from doing so. The result is a fragile system, as object validation ceases to be a priority and becomes solely dependent on the execution flow.</p><p>Another problem is rule duplication. In large systems, the same rule is often needed in more than one use case. When it is in the Service, it ends up being copied to other services, handlers or jobs, which increases the risk of inconsistency and hinders business evolution.</p><p>Finally, the Service Layer is responsible for orchestrating use cases, coordinating repositories, transactions and external calls. When it validates domain rules, it mixes responsibilities and becomes a central point of complexity, bloated with if statements and exceptions that don&rsquo;t belong to it.</p><p>The example below shows what a Service class looks like with business rules defined in it:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">OrderService</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> OrderRepository _orderRepository<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">OrderService</span><span class="token punctuation">(</span>OrderRepository orderRepository<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _orderRepository <span class="token operator">=</span> orderRepository<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">Create</span><span class="token punctuation">(</span>CreateOrderDto dto<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>dto<span class="token punctuation">.</span>Total <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Exception</span><span class="token punctuation">(</span><span class="token string">"Total must be greater than zero"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>dto<span class="token punctuation">.</span>Items<span class="token punctuation">.</span><span class="token function">Any</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Exception</span><span class="token punctuation">(</span><span class="token string">"Order must have at least one item"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">var</span> order <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Order</span><span class="token punctuation">(</span>dto<span class="token punctuation">.</span>Total<span class="token punctuation">,</span> dto<span class="token punctuation">.</span>Items<span class="token punctuation">)</span><span class="token punctuation">;</span>
        _orderRepository<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Note that the logic of <code>if</code> and <code>else</code> statements remains the same, it has only changed location, allowing objects to change state and create corrupted states.</p><h3 id="using-data-annotations">3. Using Data Annotations</h3><p>Another common form of validation is through the Data Annotations Model Binder, which is implemented using attributes placed above the properties of an entity class.</p><p>The problem is that, as they make the domain dependent on a framework, the validation only works in binding (MVC), and they may not work in non-web scenarios such as workers, messaging and tests.</p><pre class=" language-csharp"><code class="prism  language-csharp"> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Order</span>
  <span class="token punctuation">{</span>
      <span class="token punctuation">[</span>Required<span class="token punctuation">]</span>
      <span class="token punctuation">[</span><span class="token function">Range</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">double</span><span class="token punctuation">.</span>MaxValue<span class="token punctuation">)</span><span class="token punctuation">]</span>
      <span class="token keyword">public</span> <span class="token keyword">decimal</span> Total <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
      <span class="token keyword">public</span> List<span class="token operator">&lt;</span>OrderItem<span class="token operator">&gt;</span> Items <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
</code></pre><p>Note that we are requiring the Total property to have a minimum value of 1. But even so, it is not yet a secure validation because it is still possible to create an entity with an invalid state.</p><h2 id="domain-validation-with-ddd">Domain Validation with DDD</h2><p>Domain-Driven Design emphasizes the importance of keeping validations within the domain. The main reason is that this way, the domain becomes self-protecting.</p><p>Entities and aggregates cease to be passive structures (as seen in previous examples) and ensure that their rules are always respected, regardless of where or how they are used.</p><p>Another positive aspect of this approach is the model&rsquo;s coherence. When business rules are in the domain, they are closer to the concept they represent, making the code more expressive and easier to understand. Reading an entity or a behavioral method reveals the rules governing that concept. In other words, you understand the intention behind that behavior.</p><p>Finally, implementing validations in the domain makes system evolution safer. New use cases can be added without fear of breaking existing rules because the domain itself acts as a safety barrier. This reduces maintenance costs and makes the system more resilient to business growth and changes.</p><p>Next, we&rsquo;ll look at an example of a domain guided by DDD principles and analyze each point. You can access the source code in this GitHub repository: <a target="_blank" href="https://github.com/zangassis/domain-validations">Domain Validations code</a>.</p><p>Order class with Domain Validations:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Order</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> List<span class="token operator">&lt;</span>OrderItem<span class="token operator">&gt;</span> _items <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> Guid Id <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> DateTime CreatedAt <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> IReadOnlyCollection<span class="token operator">&lt;</span>OrderItem<span class="token operator">&gt;</span> Items <span class="token operator">=</span><span class="token operator">&gt;</span> _items<span class="token punctuation">.</span><span class="token function">AsReadOnly</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">decimal</span> Total <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token function">Order</span><span class="token punctuation">(</span>IEnumerable<span class="token operator">&lt;</span>OrderItem<span class="token operator">&gt;</span> items<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>items <span class="token operator">==</span> <span class="token keyword">null</span> <span class="token operator">||</span> <span class="token operator">!</span>items<span class="token punctuation">.</span><span class="token function">Any</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order must have at least one item."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        Id <span class="token operator">=</span> Guid<span class="token punctuation">.</span><span class="token function">NewGuid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow<span class="token punctuation">;</span>

        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token keyword">var</span> item <span class="token keyword">in</span> items<span class="token punctuation">)</span>
            <span class="token function">AddItem</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token function">ValidateItemsQuantity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">AddItem</span><span class="token punctuation">(</span>OrderItem item<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>item <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order item cannot be null."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        _items<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token function">RecalculateTotal</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">RecalculateTotal</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        Total <span class="token operator">=</span> _items<span class="token punctuation">.</span><span class="token function">Sum</span><span class="token punctuation">(</span>i <span class="token operator">=</span><span class="token operator">&gt;</span> i<span class="token punctuation">.</span>Subtotal<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">if</span> <span class="token punctuation">(</span>Total <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order total must be greater than zero."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">ValidateItemsQuantity</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>_items<span class="token punctuation">.</span><span class="token function">Any</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order cannot exist without items."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Item class with Domain Validations:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">OrderItem</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> Guid ProductId <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">decimal</span> Price <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">int</span> Quantity <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token keyword">decimal</span> Subtotal <span class="token operator">=</span><span class="token operator">&gt;</span> Price <span class="token operator">*</span> Quantity<span class="token punctuation">;</span>

    <span class="token keyword">protected</span> <span class="token function">OrderItem</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span> <span class="token comment">// EF</span>

    <span class="token keyword">public</span> <span class="token function">OrderItem</span><span class="token punctuation">(</span>Guid productId<span class="token punctuation">,</span> <span class="token keyword">decimal</span> price<span class="token punctuation">,</span> <span class="token keyword">int</span> quantity<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>productId <span class="token operator">==</span> Guid<span class="token punctuation">.</span>Empty<span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"ProductId is required."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">if</span> <span class="token punctuation">(</span>price <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Price must be greater than zero."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">if</span> <span class="token punctuation">(</span>quantity <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
            <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Quantity must be greater than zero."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        ProductId <span class="token operator">=</span> productId<span class="token punctuation">;</span>
        Price <span class="token operator">=</span> price<span class="token punctuation">;</span>
        Quantity <span class="token operator">=</span> quantity<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="private-setters-and-readonly-properties">Private Setters and ReadOnly Properties</h3><p>The first aspect we can notice in this new version of the Order class is that the Items property is private, which means it is inaccessible outside the class: <code>private readonly List&lt;OrderItem&gt; _items = new();</code>.</p><p>We also have a public list of items: <code>public IReadOnlyCollection&lt;OrderItem&gt; Items =&gt; _items.AsReadOnly();</code>, but note that it is defined as read-only. This means that it can be accessed by external sources, but only for reading the data and never for modification.</p><p>Another factor protecting the class properties is that, despite being public, they have private setters: <code>public Guid Id { get; private set; }</code>, preventing external sources from modifying their states.</p><h3 id="protected-constructor">Protected Constructor</h3><p>Protecting the constructor method means allowing only valid states of an entity to be created. In our example, we are defining rules within the constructor:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token function">Order</span><span class="token punctuation">(</span>IEnumerable<span class="token operator">&lt;</span>OrderItem<span class="token operator">&gt;</span> items<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>items <span class="token operator">==</span> <span class="token keyword">null</span> <span class="token operator">||</span> <span class="token operator">!</span>items<span class="token punctuation">.</span><span class="token function">Any</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order must have at least one item."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Id <span class="token operator">=</span> Guid<span class="token punctuation">.</span><span class="token function">NewGuid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow<span class="token punctuation">;</span>

    <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token keyword">var</span> item <span class="token keyword">in</span> items<span class="token punctuation">)</span>
        <span class="token function">AddItem</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token function">ValidateItemsQuantity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">AddItem</span><span class="token punctuation">(</span>OrderItem item<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>item <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order item cannot be null."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    _items<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token function">RecalculateTotal</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">RecalculateTotal</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    Total <span class="token operator">=</span> _items<span class="token punctuation">.</span><span class="token function">Sum</span><span class="token punctuation">(</span>i <span class="token operator">=</span><span class="token operator">&gt;</span> i<span class="token punctuation">.</span>Subtotal<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span>Total <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order total must be greater than zero."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">ValidateItemsQuantity</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>_items<span class="token punctuation">.</span><span class="token function">Any</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainException</span><span class="token punctuation">(</span><span class="token string">"Order cannot exist without items."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>Note that the item quantity validation is performed within the constructor. That is, when creating a new object state, values are also set for the <code>Id</code> and <code>CreatedAt</code> properties. Finally, the <code>ValidateItemsQuantity</code> method verifies if the private property <code>_items</code> has actually been loaded with items, adding an extra layer of validation. In this way, the database will only receive valid states, a corrupted or incomplete entity will never be inserted, enabling data consistency.</p><h3 id="domain-exceptions">Domain Exceptions</h3><p>Domain exceptions are errors thrown when a business rule (invariant) is violated. Unlike technical failures such as database outages or timeouts, domain exceptions represent violations of the system&rsquo;s business rules, such as an order without items, for example.</p><p>Domain exceptions are important because they help prevent an entity or aggregate from existing in an invalid state. If a rule is broken, the domain needs to react immediately, and the exception is a suitable mechanism to keep inconsistent data from reaching the database or being manipulated in any way.</p><p>In the previous example, we validated whether the value was less than zero and whether the list of items was empty. If either condition is met, a <code>DomainException</code> will be thrown, which is a custom exception. The code below shows how the exception is implemented:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">namespace</span> DomainValidation<span class="token punctuation">;</span>

<span class="token punctuation">[</span>Serializable<span class="token punctuation">]</span>
<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">DomainException</span> <span class="token punctuation">:</span> Exception
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token function">DomainException</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token function">DomainException</span><span class="token punctuation">(</span><span class="token keyword">string</span><span class="token operator">?</span> message<span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token keyword">base</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token function">DomainException</span><span class="token punctuation">(</span><span class="token keyword">string</span><span class="token operator">?</span> message<span class="token punctuation">,</span> Exception<span class="token operator">?</span> innerException<span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token keyword">base</span><span class="token punctuation">(</span>message<span class="token punctuation">,</span> innerException<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="is-fluentvalidation-still-useful">Is FluentValidation Still Useful?</h3><p>FluentValidation is a widely used .NET library for validations, and even in scenarios where we use the domain validation approach, it certainly remains useful.</p><p>The main functionality of FluentValidation is to validate input data, not business rules. Therefore, by using it in the application, we can obtain an extra layer of validation, preventing corrupted data from reaching the domain.</p><p>The code below demonstrates how FluentValidation can be used to validate input data:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">using</span> FluentValidation<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">CreateOrderRequest</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> List<span class="token operator">&lt;</span>CreateOrderItemRequest<span class="token operator">&gt;</span> Items <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">CreateOrderRequestValidator</span> <span class="token punctuation">:</span> AbstractValidator<span class="token operator">&lt;</span>CreateOrderRequest<span class="token operator">&gt;</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token function">CreateOrderRequestValidator</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token function">RuleFor</span><span class="token punctuation">(</span>x <span class="token operator">=</span><span class="token operator">&gt;</span> x<span class="token punctuation">.</span>Items<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">NotNull</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">WithMessage</span><span class="token punctuation">(</span><span class="token string">"Items cannot be null."</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Must</span><span class="token punctuation">(</span>x <span class="token operator">=</span><span class="token operator">&gt;</span> x<span class="token punctuation">.</span><span class="token function">Any</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">WithMessage</span><span class="token punctuation">(</span><span class="token string">"Order must contain at least one item."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token function">RuleForEach</span><span class="token punctuation">(</span>x <span class="token operator">=</span><span class="token operator">&gt;</span> x<span class="token punctuation">.</span>Items<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">SetValidator</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">CreateOrderItemRequestValidator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">CreateOrderItemRequest</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> Guid ProductId <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">decimal</span> Price <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">int</span> Quantity <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">CreateOrderItemRequestValidator</span> <span class="token punctuation">:</span> AbstractValidator<span class="token operator">&lt;</span>CreateOrderItemRequest<span class="token operator">&gt;</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token function">CreateOrderItemRequestValidator</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token function">RuleFor</span><span class="token punctuation">(</span>x <span class="token operator">=</span><span class="token operator">&gt;</span> x<span class="token punctuation">.</span>ProductId<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">NotEmpty</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">WithMessage</span><span class="token punctuation">(</span><span class="token string">"ProductId is required."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token function">RuleFor</span><span class="token punctuation">(</span>x <span class="token operator">=</span><span class="token operator">&gt;</span> x<span class="token punctuation">.</span>Price<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">GreaterThan</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">WithMessage</span><span class="token punctuation">(</span><span class="token string">"Price must be greater than zero."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token function">RuleFor</span><span class="token punctuation">(</span>x <span class="token operator">=</span><span class="token operator">&gt;</span> x<span class="token punctuation">.</span>Quantity<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">GreaterThan</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">WithMessage</span><span class="token punctuation">(</span><span class="token string">"Quantity must be greater than zero."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><h2 id="when-out-of-domain-validations-make-sense">When Out-of-Domain Validations Make Sense</h2><p>Validations in the Controller and Service classes make sense when they are not business rules, but rather validations such as security, flow or format.</p><p>It is common to perform authentication/authorization validations in the Controller, because this is the responsibility of the edge (API), and if a user or system is not properly authenticated/authorized, it should not have access to the domain or the rest of the system; it should be blocked at the entrance.</p><p>The Service class, on the other hand, can have validations used to manage operational flows, for example, checking if the customer exists before creating an order, checking if the product exists, checking if there is already an open order for the customer.</p><p>In this way, we do not validate the internal state of the entity. Instead, we validate the interactions between aggregates or external systems.</p><h2 id="conclusion">Conclusion</h2><p>Implementing domain validations in an application means explicitly stating the reason for that domain&rsquo;s existence and the rules that determine its behavior. Furthermore, domain validations keep an invalid object from reaching the database, preventing future bugs and the resulting damage.</p><p>Throughout this post, we&rsquo;ve seen examples of incorrectly implemented validations, scattered across controllers or services, resulting in fragile and difficult-to-maintain code. In contrast, we implemented validations directly on the entities, applying DDD principles.</p><p>I hope this post has helped you see the domain as the heart of the application and the importance of keeping it consistent, protected and expressive.</p><hr /><p><strong>More on DDD: </strong><a href="https://www.telerik.com/blogs/getting-started-domain-driven-design-aspnet-core" target="_blank">Getting Started with Domain-Driven Design in ASP.NET Core</a></p><img src="https://feeds.telerik.com/link/10827/17344160.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:6474f4bb-8be6-4855-88f7-d84c8ef0da59</id>
    <title type="text">How to Build a Multi-Tenant SaaS API with NestJS and Postgres Row-Level Security</title>
    <summary type="text">We’ll build a multi-tenant task management API where customer data is isolated automatically at the database level. We’ll use Postgres Row-Level Security to enforce tenant isolation, with NestJS as our application framework and TypeORM to interact with the database.</summary>
    <published>2026-05-14T12:50:00Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Christian Nwamba </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17341162/how-to-build-multi-tenant-saas-api-nestjs-postgres-row-level-security"/>
    <content type="text"><![CDATA[<p><span class="featured">We&rsquo;ll build a multi-tenant task management API where customer data is isolated automatically at the database level. We&rsquo;ll use Postgres Row-Level Security to enforce tenant isolation, with NestJS as our application framework and TypeORM to interact with the database.</span></p><p>In this post, we will build a multi-tenant task management API where customer data is isolated automatically at the database level. We&rsquo;ll use <a target="_blank" href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html">Postgres Row-Level Security</a> to enforce tenant isolation, with <a target="_blank" href="https://nestjs.com/">NestJS</a> as our application framework and <a target="_blank" href="https://typeorm.io/">TypeORM</a> to interact with the database.</p><p>You&rsquo;ll learn how to set up RLS policies that filter queries by tenant, extract tenant information from JWT tokens, manage tenant onboarding and test that isolation works even against direct ID access attempts. By the end, you&rsquo;ll have a working multi-tenant SaaS API where tenants only see their own data, enforced at the database layer rather than in application code.</p><h2 id="prerequisites">Prerequisites</h2><p>To follow along, you&rsquo;ll need:</p><ul><li>Node.js installed</li><li><a target="_blank" href="https://www.postgresql.org/">Postgres</a> installed locally</li><li>Basic knowledge of NestJS and <a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a></li><li>Basic knowledge of HTTP, RESTful APIs and cURL</li><li>Basic knowledge of JWT authentication</li></ul><aside><hr /><div class="row"><div class="col-4 u-normal-full u-small-mb0"><h4 class="u-fs20 u-fw5 u-lh125 u-mb0">Introduction to JSON Web Tokens (JWT)
            </h4></div><div class="col-8"><p class="u-fs16 u-mb0"><a target="_blank" href="https://www.telerik.com/blogs/introduction-json-web-tokens-jwt">Learn the basics about using JWTs</a> to implement authorization in web applications.
            </p></div></div><hr class="u-mb3" /></aside><h2 id="what-is-multi-tenancy">What Is Multi-Tenancy?</h2><p>Multi-tenancy is an architecture where one application instance serves multiple customers (tenants). Each tenant&rsquo;s data is completely isolated from other tenants. If Company A creates a task, Company B should not be able to see it.</p><p>There are three main ways to achieve this:</p><ul><li><strong>Database per tenant:</strong> Each tenant gets their own database. This provides maximum isolation but is expensive and difficult to maintain at scale.</li><li><strong>Schema per tenant:</strong> All tenants share one database, but each gets their own schema. This is cheaper than separate databases, but migration management becomes complex.</li><li><strong>Shared schema with RLS:</strong> All tenants share the same database and schema. Isolation is enforced by the database itself using Row-Level Security policies.</li></ul><p>For a quick MVP build, the shared schema approach with RLS provides strong isolation without operational complexity.</p><h2 id="what-is-row-level-security">What Is Row-Level Security?</h2><p>Row-Level Security (RLS) is a Postgres feature that lets you define policies controlling which rows users can see or modify.</p><p>When RLS is enabled on a table, Postgres automatically filters query results based on the policies you define. Even if our application code doesn&rsquo;t add <code>WHERE tenant_id = ?</code>, the database allows tenants to only see their own data.</p><p>When a query runs, Postgres checks the policy conditions and rewrites the query. If our policy says <code>USING (tenant_id = current_setting('app.tenant_id'))</code>, then <code>SELECT * FROM tasks</code> becomes <code>SELECT * FROM tasks WHERE tenant_id = current_setting('app.tenant_id')</code>.<br />This moves security from the application layer (where programmers might make mistakes) to the database layer (where it is enforced automatically).</p><h2 id="project-setup">Project Setup</h2><p>First, create a NestJS project:</p><pre class=" language-shell"><code class="prism  language-shell">nest new task-management-api
cd task-management-api
</code></pre><p>Next, run the following command in your terminal to install our dependencies:</p><pre class=" language-shell"><code class="prism  language-shell">npm install @nestjs/config @nestjs/typeorm typeorm pg bcrypt @nestjs/jwt @nestjs/passport passport passport-jwt nestjs-cls uuid
npm install --save-dev @types/bcrypt @types/uuid @types/passport-jwt
</code></pre><p>Here&rsquo;s what each package does:</p><ul><li><strong>@nestjs/config:</strong> Manages environment variables</li><li><strong>@nestjs/typeorm and typeorm:</strong> ORM for Postgres</li><li><strong>pg:</strong> Postgres driver</li><li><strong>bcrypt:</strong> Password hashing</li><li><strong>@nestjs/jwt and @nestjs/passport:</strong> Create and validate JWT tokens</li><li><strong>passport and passport-jwt:</strong> Provide authentication strategies</li><li><strong>nestjs-cls:</strong> Request-scoped context storage for tracking tenants</li><li><strong>uuid:</strong> Creates unique IDs</li></ul><p>Next, create a <code>.env</code> file at the root of your project and add the following to it:</p><pre class=" language-js"><code class="prism  language-js">DB_HOST<span class="token operator">=</span>localhost
DB_PORT<span class="token operator">=</span><span class="token number">5432</span>
DB_USERNAME<span class="token operator">=</span>app_user
DB_PASSWORD<span class="token operator">=</span>newpassword123
DB_NAME<span class="token operator">=</span>task_management

JWT_SECRET<span class="token operator">=</span>your<span class="token operator">-</span>secret<span class="token operator">-</span>key<span class="token operator">-</span>change<span class="token operator">-</span><span class="token keyword">this</span><span class="token operator">-</span><span class="token keyword">in</span><span class="token operator">-</span>production
JWT_EXPIRATION<span class="token operator">=</span>24h
</code></pre><p>The variables above configure our database connection and JWT settings.</p><p>Now update your app.module.ts file to load these variables:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>
    ConfigModule<span class="token punctuation">.</span><span class="token function">forRoot</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      isGlobal<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
      envFilePath<span class="token punctuation">:</span> <span class="token string">'.env'</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token comment">// ... we will add other modules here later</span>
  <span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h2 id="database-setup">Database Setup</h2><p>Let&rsquo;s set up our Postgres database with the tables and RLS policies needed for tenant isolation.</p><h3 id="connect-to-postgres">Connect to Postgres</h3><p>Open the psql shell (search for &ldquo;SQL Shell (psql)&rdquo; in your Start menu). When prompted:</p><ul><li><strong>Server:</strong> localhost</li><li><strong>Database:</strong> postgres</li><li><strong>Port:</strong> 5432</li><li><strong>Username:</strong> postgres</li><li><strong>Password:</strong> [your postgres password]</li></ul><h3 id="create-database-and-user">Create Database and User</h3><p>Run these commands in the psql shell:</p><pre class=" language-shell"><code class="prism  language-shell">CREATE DATABASE task_management;
CREATE USER app_user WITH PASSWORD 'newpassword123' NOSUPERUSER;
GRANT CONNECT ON DATABASE task_management TO app_user;
\c task_management
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
GRANT USAGE ON SCHEMA public TO app_user;
GRANT CREATE ON SCHEMA public TO app_user;
\q
</code></pre><p>We create <code>app_user</code> with <code>NOSUPERUSER</code> because superusers bypass RLS policies, which would completely break our isolation.</p><h3 id="create-tables-and-rls-policies">Create Tables and RLS Policies</h3><p>Open psql again and connect as app_user:</p><pre class=" language-shell"><code class="prism  language-shell">Server: localhost
Database: task_management
Port: 5432
Username: app_user
Password: newpassword123
</code></pre><p>Now, run the complete SQL setup:</p><pre class=" language-shell"><code class="prism  language-shell">-- Create tenants table (no RLS needed here)
CREATE TABLE tenants (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Create users table (NO RLS - needed for login)
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  email VARCHAR(255) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Create tasks table
CREATE TABLE tasks (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  title VARCHAR(255) NOT NULL,
  description TEXT,
  status VARCHAR(50) DEFAULT 'pending',
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Enable RLS ONLY on tasks table
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

-- Force RLS even for table owner
ALTER TABLE tasks FORCE ROW LEVEL SECURITY;

-- Create RLS policy for tasks with both USING and WITH CHECK
CREATE POLICY tenant_isolation_tasks ON tasks
  USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid)
  WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);

-- Create indexes for performance
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
CREATE INDEX idx_tasks_tenant_id ON tasks(tenant_id);
CREATE INDEX idx_tasks_tenant_status ON tasks(tenant_id, status);

-- Exit psql
\q
</code></pre><p>The <code>ENABLE ROW LEVEL SECURITY</code> command turns on RLS for the tasks table. The <code>FORCE ROW LEVEL SECURITY</code> command applies RLS even to the table owner, helping prevent unintended data exposure during manual database work.</p><p>The <code>USING</code> clause in our policy checks if the row&rsquo;s tenant_id corresponds to the session variable app.current_tenant_id. The <code>WITH CHECK</code> clause validates INSERT and UPDATE operations to ensure the tenant_id matches. The <code>true</code> parameter in current_setting tells Postgres to return NULL instead of throwing an error if the variable isn&rsquo;t set.</p><p>We&rsquo;re using UUID primary keys instead of sequential integers because with integers, a malicious tenant could guess other tenants&rsquo; IDs and probe for data leaks through foreign key violations.</p><blockquote><p>The users table doesn&rsquo;t have RLS enabled. This is intentional because we need to query users across tenants during login, before we have a tenant context.</p></blockquote><h2 id="project-structure">Project Structure</h2><p>Our project structure will look like this:</p><pre class=" language-js"><code class="prism  language-js">src<span class="token operator">/</span>
├── auth<span class="token operator">/</span>
│   ├── guards<span class="token operator">/</span>
│   │   └── jwt<span class="token operator">-</span>auth<span class="token punctuation">.</span>guard<span class="token punctuation">.</span>ts
│   ├── strategies<span class="token operator">/</span>
│   │   └── jwt<span class="token punctuation">.</span>strategy<span class="token punctuation">.</span>ts
│   ├── auth<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>ts
│   ├── auth<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
│   ├── auth<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   ├── auth<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
│   └── auth<span class="token punctuation">.</span>service<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
├── database<span class="token operator">/</span>
│   ├── database<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   └── tenant<span class="token operator">-</span>context<span class="token punctuation">.</span>interceptor<span class="token punctuation">.</span>ts
├── middleware<span class="token operator">/</span>
│   └── tenant<span class="token punctuation">.</span>middleware<span class="token punctuation">.</span>ts
├── tasks<span class="token operator">/</span>
│   ├── entities<span class="token operator">/</span>
│   │   └── task<span class="token punctuation">.</span>entity<span class="token punctuation">.</span>ts
│   ├── tasks<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>ts
│   ├── tasks<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
│   ├── tasks<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   └── tasks<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
├── tenants<span class="token operator">/</span>
│   ├── entities<span class="token operator">/</span>
│   │   └── tenant<span class="token punctuation">.</span>entity<span class="token punctuation">.</span>ts
│   ├── tenants<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>ts
│   ├── tenants<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
│   ├── tenants<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   └── tenants<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
├── users<span class="token operator">/</span>
│   ├── entities<span class="token operator">/</span>
│   │   └── user<span class="token punctuation">.</span>entity<span class="token punctuation">.</span>ts
│   ├── users<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   ├── users<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
│   └── users<span class="token punctuation">.</span>service<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
├── app<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
└── main<span class="token punctuation">.</span>ts
</code></pre><p>Use the commands below to create the basic structure:</p><pre class=" language-shell"><code class="prism  language-shell">nest g module tenants &amp;&amp; \
nest g controller tenants &amp;&amp; \
nest g service tenants &amp;&amp; \
nest g module auth &amp;&amp; \
nest g controller auth &amp;&amp; \
nest g service auth &amp;&amp; \
nest g module tasks &amp;&amp; \
nest g controller tasks &amp;&amp; \
nest g service tasks &amp;&amp; \
nest g module users &amp;&amp; \
nest g service users &amp;&amp; \
nest g module database
</code></pre><p>We&rsquo;ll also need to manually create the entities, guards, strategies, middleware and interceptor files later on.</p><h2 id="configure-typeorm">Configure TypeORM</h2><p>TypeORM needs to connect to Postgres and recognize our entities.</p><p>Update the <code>src/app.module.ts</code> file to add TypeORM:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module<span class="token punctuation">,</span> MiddlewareConsumer<span class="token punctuation">,</span> RequestMethod <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigModule<span class="token punctuation">,</span> ConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TypeOrmModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ClsModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'nestjs-cls'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TenantMiddleware <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./middleware/tenant.middleware'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TenantsModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tenants/tenants.module'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AuthModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./auth/auth.module'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> UsersModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./users/users.module'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TasksModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tasks/tasks.module'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> DatabaseModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./database/database.module'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>
    ConfigModule<span class="token punctuation">.</span><span class="token function">forRoot</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      isGlobal<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
      envFilePath<span class="token punctuation">:</span> <span class="token string">'.env'</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    TypeOrmModule<span class="token punctuation">.</span><span class="token function">forRootAsync</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      inject<span class="token punctuation">:</span> <span class="token punctuation">[</span>ConfigService<span class="token punctuation">]</span><span class="token punctuation">,</span>
      useFactory<span class="token punctuation">:</span> <span class="token punctuation">(</span>config<span class="token punctuation">:</span> ConfigService<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">(</span><span class="token punctuation">{</span>
        <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'postgres'</span><span class="token punctuation">,</span>
        host<span class="token punctuation">:</span> config<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'DB_HOST'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        port<span class="token punctuation">:</span> config<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'DB_PORT'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        username<span class="token punctuation">:</span> config<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'DB_USERNAME'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        password<span class="token punctuation">:</span> config<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'DB_PASSWORD'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        database<span class="token punctuation">:</span> config<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'DB_NAME'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        entities<span class="token punctuation">:</span> <span class="token punctuation">[</span>__dirname <span class="token operator">+</span> <span class="token string">'/**/*.entity{.ts,.js}'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        synchronize<span class="token punctuation">:</span> <span class="token keyword">false</span><span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    ClsModule<span class="token punctuation">.</span><span class="token function">forRoot</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      global<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
      middleware<span class="token punctuation">:</span> <span class="token punctuation">{</span> mount<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    AuthModule<span class="token punctuation">,</span>
    TenantsModule<span class="token punctuation">,</span>
    UsersModule<span class="token punctuation">,</span>
    TasksModule<span class="token punctuation">,</span>
    DatabaseModule<span class="token punctuation">,</span>
  <span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span>
  <span class="token function">configure</span><span class="token punctuation">(</span>consumer<span class="token punctuation">:</span> MiddlewareConsumer<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    consumer
      <span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span>TenantMiddleware<span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">exclude</span><span class="token punctuation">(</span>
        <span class="token punctuation">{</span> path<span class="token punctuation">:</span> <span class="token string">'tenants/register'</span><span class="token punctuation">,</span> method<span class="token punctuation">:</span> RequestMethod<span class="token punctuation">.</span>POST <span class="token punctuation">}</span><span class="token punctuation">,</span>
        <span class="token punctuation">{</span> path<span class="token punctuation">:</span> <span class="token string">'auth/login'</span><span class="token punctuation">,</span> method<span class="token punctuation">:</span> RequestMethod<span class="token punctuation">.</span>POST <span class="token punctuation">}</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">forRoutes</span><span class="token punctuation">(</span><span class="token string">'*'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>We set <code>synchronize: false</code> because we created our tables manually with RLS policies. If we let TypeORM auto-generate tables, it won&rsquo;t add the RLS policies.</p><p>The ClsModule provides request-scoped storage that persists across async operations. When we set a value in the CLS store, it is only accessible within that specific request&rsquo;s execution context. The ClsModule is configured with <code>global: true</code>, so it is available throughout the entire application without importing it into every module. The <code>middleware: { mount: true }</code> setting tells CLS to automatically track the request context as it passes through our application.</p><p>The configure method at the bottom sets up our tenant middleware to run on all routes except registration and login. These two endpoints need to be public because users don&rsquo;t have tokens yet when they&rsquo;re signing up or logging in. We&rsquo;ll create the middleware file and add the logic later.</p><h2 id="creating-entities">Creating Entities</h2><h3 id="tenant-entity">Tenant Entity</h3><p>Create a file named <code>tenant.entity.ts</code> in the <code>src/tenants/entities/</code> folder:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Entity<span class="token punctuation">,</span> PrimaryGeneratedColumn<span class="token punctuation">,</span> Column<span class="token punctuation">,</span> CreateDateColumn <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span>

@<span class="token function">Entity</span><span class="token punctuation">(</span><span class="token string">'tenants'</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">Tenant</span> <span class="token punctuation">{</span>
  @<span class="token function">PrimaryGeneratedColumn</span><span class="token punctuation">(</span><span class="token string">'uuid'</span><span class="token punctuation">)</span>
  id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span> length<span class="token punctuation">:</span> <span class="token number">255</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  name<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">CreateDateColumn</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">:</span> <span class="token string">'created_at'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  createdAt<span class="token punctuation">:</span> Date<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="user-entity">User Entity</h3><p>Create a file named <code>user.entity.ts</code> in the <code>src/users/entities/</code> folder:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Entity<span class="token punctuation">,</span> PrimaryGeneratedColumn<span class="token punctuation">,</span> Column<span class="token punctuation">,</span> CreateDateColumn <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span>

@<span class="token function">Entity</span><span class="token punctuation">(</span><span class="token string">'users'</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token punctuation">{</span>
  @<span class="token function">PrimaryGeneratedColumn</span><span class="token punctuation">(</span><span class="token string">'uuid'</span><span class="token punctuation">)</span>
  id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">:</span> <span class="token string">'tenant_id'</span><span class="token punctuation">,</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'uuid'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  tenantId<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span> length<span class="token punctuation">:</span> <span class="token number">255</span><span class="token punctuation">,</span> unique<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  email<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">:</span> <span class="token string">'password_hash'</span><span class="token punctuation">,</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span> length<span class="token punctuation">:</span> <span class="token number">255</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  passwordHash<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">CreateDateColumn</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">:</span> <span class="token string">'created_at'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  createdAt<span class="token punctuation">:</span> Date<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="task-entity">Task Entity</h3><p>Create a file named <code>task.entity.ts</code> in the <code>src/tasks/entities/</code> folder:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Entity<span class="token punctuation">,</span> PrimaryGeneratedColumn<span class="token punctuation">,</span> Column<span class="token punctuation">,</span> CreateDateColumn<span class="token punctuation">,</span> UpdateDateColumn <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span>

@<span class="token function">Entity</span><span class="token punctuation">(</span><span class="token string">'tasks'</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">Task</span> <span class="token punctuation">{</span>
  @<span class="token function">PrimaryGeneratedColumn</span><span class="token punctuation">(</span><span class="token string">'uuid'</span><span class="token punctuation">)</span>
  id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">:</span> <span class="token string">'tenant_id'</span><span class="token punctuation">,</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'uuid'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  tenantId<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span> length<span class="token punctuation">:</span> <span class="token number">255</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  title<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'text'</span><span class="token punctuation">,</span> nullable<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  description<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">Column</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token keyword">type</span><span class="token punctuation">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span> length<span class="token punctuation">:</span> <span class="token number">50</span><span class="token punctuation">,</span> <span class="token keyword">default</span><span class="token punctuation">:</span> <span class="token string">'pending'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  status<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  @<span class="token function">CreateDateColumn</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">:</span> <span class="token string">'created_at'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  createdAt<span class="token punctuation">:</span> Date<span class="token punctuation">;</span>

  @<span class="token function">UpdateDateColumn</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">:</span> <span class="token string">'updated_at'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  updatedAt<span class="token punctuation">:</span> Date<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h2 id="building-the-tenant-context-system">Building the Tenant Context System</h2><p>The main challenge in using RLS with Node.js is propagating the tenant ID through asynchronous operations.</p><p>Since Node.js handles multiple requests concurrently, we can&rsquo;t use global variables, as they would be overwritten. We need request-scoped storage that persists across async boundaries.</p><h3 id="creating-the-tenant-middleware">Creating the Tenant Middleware</h3><p>The middleware extracts the tenant ID from the JWT and stores it in the CLS context.</p><p>Create a file named <code>tenant.middleware.ts</code> in the <code>src/middleware/</code> folder:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable<span class="token punctuation">,</span> NestMiddleware<span class="token punctuation">,</span> UnauthorizedException <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Request<span class="token punctuation">,</span> Response<span class="token punctuation">,</span> NextFunction <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'express'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ClsService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'nestjs-cls'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> JwtService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/jwt'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TenantMiddleware</span> <span class="token keyword">implements</span> <span class="token class-name">NestMiddleware</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    <span class="token keyword">private</span> readonly cls<span class="token punctuation">:</span> ClsService<span class="token punctuation">,</span>
    <span class="token keyword">private</span> readonly jwtService<span class="token punctuation">:</span> JwtService<span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token function">use</span><span class="token punctuation">(</span>req<span class="token punctuation">:</span> Request<span class="token punctuation">,</span> res<span class="token punctuation">:</span> Response<span class="token punctuation">,</span> next<span class="token punctuation">:</span> NextFunction<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> authHeader <span class="token operator">=</span> req<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>authorization<span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>authHeader <span class="token operator">||</span> <span class="token operator">!</span>authHeader<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'Bearer '</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">UnauthorizedException</span><span class="token punctuation">(</span><span class="token string">'Missing or invalid authorization header'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">const</span> token <span class="token operator">=</span> authHeader<span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span><span class="token number">7</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
      <span class="token keyword">const</span> payload <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>jwtService<span class="token punctuation">.</span><span class="token function">verify</span><span class="token punctuation">(</span>token<span class="token punctuation">)</span><span class="token punctuation">;</span>
      
      <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>payload<span class="token punctuation">.</span>tenantId<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">UnauthorizedException</span><span class="token punctuation">(</span><span class="token string">'Token missing tenant context'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>

      <span class="token keyword">this</span><span class="token punctuation">.</span>cls<span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span><span class="token string">'TENANT_ID'</span><span class="token punctuation">,</span> payload<span class="token punctuation">.</span>tenantId<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">this</span><span class="token punctuation">.</span>cls<span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span><span class="token string">'USER_ID'</span><span class="token punctuation">,</span> payload<span class="token punctuation">.</span>sub<span class="token punctuation">)</span><span class="token punctuation">;</span>
      
      <span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">UnauthorizedException</span><span class="token punctuation">(</span><span class="token string">'Invalid token'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
</code></pre><p>This middleware runs on every request except the excluded routes (registration and login). It extracts the JWT from the Authorization header, verifies it and stores the tenantId in the CLS context. Any service can then retrieve this value using <code>this.cls.get('TENANT_ID')</code>.</p><h2 id="typeorm-with-rls-the-challenge">TypeORM with RLS: The Challenge</h2><p>TypeORM manages a pool of database connections that are reused across requests. When you call <code>repository.find()</code>, TypeORM borrows a connection, runs our query, then returns that connection to the pool for the next request to use.</p><p>The issue is there&rsquo;s no way to tell TypeORM &ldquo;before you run this query, first execute <code>SET app.current_tenant_id = ...</code> on this specific connection.&rdquo; By the time our service code runs, TypeORM has already grabbed the connection and is ready to execute.</p><p>The solution is to bypass TypeORM&rsquo;s automatic connection management and use QueryRunner instead. This gives us manual control. We can grab a connection, set the tenant context, run queries and then release it&mdash;all within a single transaction.</p><h3 id="creating-the-tenant-context-interceptor">Creating the Tenant Context Interceptor</h3><p>Let&rsquo;s create an interceptor that wraps every request in a transaction and sets the tenant context.</p><p>Create a file called <code>tenant-context.interceptor.ts</code> in the <code>src/database/</code>:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span>
  Injectable<span class="token punctuation">,</span>
  NestInterceptor<span class="token punctuation">,</span>
  ExecutionContext<span class="token punctuation">,</span>
  CallHandler<span class="token punctuation">,</span>
<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Observable<span class="token punctuation">,</span> <span class="token keyword">from</span><span class="token punctuation">,</span> lastValueFrom <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'rxjs'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> DataSource <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ClsService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'nestjs-cls'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TenantContextInterceptor</span> <span class="token keyword">implements</span> <span class="token class-name">NestInterceptor</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    <span class="token keyword">private</span> dataSource<span class="token punctuation">:</span> DataSource<span class="token punctuation">,</span>
    <span class="token keyword">private</span> cls<span class="token punctuation">:</span> ClsService<span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token function">intercept</span><span class="token punctuation">(</span>context<span class="token punctuation">:</span> ExecutionContext<span class="token punctuation">,</span> next<span class="token punctuation">:</span> CallHandler<span class="token punctuation">)</span><span class="token punctuation">:</span> Observable<span class="token operator">&lt;</span><span class="token keyword">any</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> tenantId <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>cls<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'TENANT_ID'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>tenantId<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> next<span class="token punctuation">.</span><span class="token function">handle</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">return</span> <span class="token keyword">from</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setupTransaction</span><span class="token punctuation">(</span>tenantId<span class="token punctuation">,</span> next<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token keyword">async</span> <span class="token function">setupTransaction</span><span class="token punctuation">(</span>tenantId<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> next<span class="token punctuation">:</span> CallHandler<span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span><span class="token keyword">any</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> queryRunner <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>dataSource<span class="token punctuation">.</span><span class="token function">createQueryRunner</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">connect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">startTransaction</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">try</span> <span class="token punctuation">{</span>
      <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>
        <span class="token template-string"><span class="token string">`SELECT set_config('app.current_tenant_id', $1, TRUE)`</span></span><span class="token punctuation">,</span>
        <span class="token punctuation">[</span>tenantId<span class="token punctuation">]</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token punctuation">;</span>

      <span class="token keyword">this</span><span class="token punctuation">.</span>cls<span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span><span class="token string">'QUERY_RUNNER'</span><span class="token punctuation">,</span> queryRunner<span class="token punctuation">)</span><span class="token punctuation">;</span>

      <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">lastValueFrom</span><span class="token punctuation">(</span>next<span class="token punctuation">.</span><span class="token function">handle</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

      <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">commitTransaction</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">return</span> result<span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">rollbackTransaction</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">throw</span> error<span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span>
      <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">release</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">this</span><span class="token punctuation">.</span>cls<span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span><span class="token string">'QUERY_RUNNER'</span><span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>This interceptor creates a new transaction for every request, sets the tenant ID using <code>set_config</code>, and stores the QueryRunner in CLS so services can access it.</p><p>The <code>TRUE</code> parameter in <code>set_config</code> is very important; it makes the setting local to this transaction. When the transaction commits or rolls back, the setting is automatically cleared. This prevents the &ldquo;leaky tenant&rdquo; problem, where one request&rsquo;s tenant ID bleeds into another request using the same pooled connection.</p><p>Update the <code>src/database/database.module.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TenantContextInterceptor <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tenant-context.interceptor'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  providers<span class="token punctuation">:</span> <span class="token punctuation">[</span>TenantContextInterceptor<span class="token punctuation">]</span><span class="token punctuation">,</span>
  exports<span class="token punctuation">:</span> <span class="token punctuation">[</span>TenantContextInterceptor<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">DatabaseModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h2 id="building-services-with-rls">Building Services with RLS</h2><p>Now, let&rsquo;s set up our services to use the QueryRunner from CLS instead of the standard repository.</p><h3 id="tenants-service">Tenants Service</h3><p>Update the <code>src/tenants/tenants.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable<span class="token punctuation">,</span> ConflictException <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> InjectRepository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Repository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Tenant <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./entities/tenant.entity'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TenantsService</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    @<span class="token function">InjectRepository</span><span class="token punctuation">(</span>Tenant<span class="token punctuation">)</span>
    <span class="token keyword">private</span> tenantsRepo<span class="token punctuation">:</span> Repository<span class="token operator">&lt;</span>Tenant<span class="token operator">&gt;</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">create</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Tenant<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> existing <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tenantsRepo<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span><span class="token punctuation">{</span> where<span class="token punctuation">:</span> <span class="token punctuation">{</span> name <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>existing<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">ConflictException</span><span class="token punctuation">(</span><span class="token string">'Tenant name already exists'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">const</span> tenant <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tenantsRepo<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tenantsRepo<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>tenant<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">findById</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Tenant <span class="token operator">|</span> <span class="token keyword">null</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tenantsRepo<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span><span class="token punctuation">{</span> where<span class="token punctuation">:</span> <span class="token punctuation">{</span> id <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The tenants table doesn&rsquo;t have RLS enabled, so we can use the standard repository here.</p><h3 id="users-service">Users Service</h3><p>Update the <code>src/users/users.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> InjectRepository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Repository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> User <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./entities/user.entity'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> bcrypt <span class="token keyword">from</span> <span class="token string">'bcrypt'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UsersService</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    @<span class="token function">InjectRepository</span><span class="token punctuation">(</span>User<span class="token punctuation">)</span>
    <span class="token keyword">private</span> usersRepo<span class="token punctuation">:</span> Repository<span class="token operator">&lt;</span>User<span class="token operator">&gt;</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">create</span><span class="token punctuation">(</span>tenantId<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> email<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> password<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>User<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> passwordHash <span class="token operator">=</span> <span class="token keyword">await</span> bcrypt<span class="token punctuation">.</span><span class="token function">hash</span><span class="token punctuation">(</span>password<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> user <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepo<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      tenantId<span class="token punctuation">,</span>
      email<span class="token punctuation">,</span>
      passwordHash<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepo<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>user<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">findByEmail</span><span class="token punctuation">(</span>email<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>User <span class="token operator">|</span> <span class="token keyword">null</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepo<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span><span class="token punctuation">{</span> where<span class="token punctuation">:</span> <span class="token punctuation">{</span> email <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Update the <code>src/users/users.module.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TypeOrmModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> UsersService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./users.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> User <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./entities/user.entity'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>TypeOrmModule<span class="token punctuation">.</span><span class="token function">forFeature</span><span class="token punctuation">(</span><span class="token punctuation">[</span>User<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  providers<span class="token punctuation">:</span> <span class="token punctuation">[</span>UsersService<span class="token punctuation">]</span><span class="token punctuation">,</span>
  exports<span class="token punctuation">:</span> <span class="token punctuation">[</span>UsersService<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UsersModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h3 id="tasks-service">Tasks Service</h3><p>Update the <code>src/tasks/tasks.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable<span class="token punctuation">,</span> NotFoundException <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> InjectRepository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Repository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ClsService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'nestjs-cls'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Task <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./entities/task.entity'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TasksService</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    @<span class="token function">InjectRepository</span><span class="token punctuation">(</span>Task<span class="token punctuation">)</span>
    <span class="token keyword">private</span> tasksRepo<span class="token punctuation">:</span> Repository<span class="token operator">&lt;</span>Task<span class="token operator">&gt;</span><span class="token punctuation">,</span>
    <span class="token keyword">private</span> cls<span class="token punctuation">:</span> ClsService<span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">getManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> queryRunner <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>cls<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'QUERY_RUNNER'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> queryRunner <span class="token operator">?</span> queryRunner<span class="token punctuation">.</span>manager <span class="token punctuation">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tasksRepo<span class="token punctuation">.</span>manager<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">create</span><span class="token punctuation">(</span>title<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> description<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Task<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> tenantId <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>cls<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'TENANT_ID'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> manager <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">const</span> task <span class="token operator">=</span> manager<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>Task<span class="token punctuation">,</span> <span class="token punctuation">{</span>
      tenantId<span class="token punctuation">,</span>
      title<span class="token punctuation">,</span>
      description<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">return</span> manager<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>task<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Task<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> manager <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> manager<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span>Task<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Task<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> manager <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> task <span class="token operator">=</span> <span class="token keyword">await</span> manager<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>Task<span class="token punctuation">,</span> <span class="token punctuation">{</span> where<span class="token punctuation">:</span> <span class="token punctuation">{</span> id <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>task<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">NotFoundException</span><span class="token punctuation">(</span><span class="token string">'Task not found'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">return</span> task<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">update</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> title<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> description<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> status<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Task<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> manager <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> task <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span>title<span class="token punctuation">)</span> task<span class="token punctuation">.</span>title <span class="token operator">=</span> title<span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>description <span class="token operator">!==</span> undefined<span class="token punctuation">)</span> task<span class="token punctuation">.</span>description <span class="token operator">=</span> description<span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>status<span class="token punctuation">)</span> task<span class="token punctuation">.</span>status <span class="token operator">=</span> status<span class="token punctuation">;</span>

    <span class="token keyword">return</span> manager<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>task<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">remove</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Task<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> manager <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> task <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">await</span> manager<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span>task<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> task<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The <code>getManager()</code> helper checks if we have a QueryRunner in CLS. If we do, we use its manager (which has the tenant context set). If not, we fall back to the standard repository manager.</p><p>Note: RLS automatically filters all queries, so <code>findAll()</code> only returns tasks belonging to the current tenant even though we don&rsquo;t explicitly filter by tenant_id. If the standard repository manager is used, however, we&rsquo;ll get null (zero rows) instead of an error because of the <code>true</code> parameter in <code>current_setting</code>.</p><p>Update the <code>src/tasks/tasks.module.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TypeOrmModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TasksController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tasks.controller'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TasksService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tasks.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Task <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./entities/task.entity'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> DatabaseModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../database/database.module'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>TypeOrmModule<span class="token punctuation">.</span><span class="token function">forFeature</span><span class="token punctuation">(</span><span class="token punctuation">[</span>Task<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span> DatabaseModule<span class="token punctuation">]</span><span class="token punctuation">,</span>
  controllers<span class="token punctuation">:</span> <span class="token punctuation">[</span>TasksController<span class="token punctuation">]</span><span class="token punctuation">,</span>
  providers<span class="token punctuation">:</span> <span class="token punctuation">[</span>TasksService<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TasksModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h2 id="tenant-registration">Tenant Registration</h2><p>The tenant registration endpoint creates a new tenant and its first user.</p><p>Update the <code>src/tenants/tenants.controller.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Controller<span class="token punctuation">,</span> Post<span class="token punctuation">,</span> Body <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TenantsService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tenants.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> UsersService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../users/users.service'</span><span class="token punctuation">;</span>

@<span class="token function">Controller</span><span class="token punctuation">(</span><span class="token string">'tenants'</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TenantsController</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    <span class="token keyword">private</span> tenantsService<span class="token punctuation">:</span> TenantsService<span class="token punctuation">,</span>
    <span class="token keyword">private</span> usersService<span class="token punctuation">:</span> UsersService<span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  @<span class="token function">Post</span><span class="token punctuation">(</span><span class="token string">'register'</span><span class="token punctuation">)</span>
  <span class="token keyword">async</span> <span class="token function">register</span><span class="token punctuation">(</span>
    @<span class="token function">Body</span><span class="token punctuation">(</span><span class="token punctuation">)</span> body<span class="token punctuation">:</span> <span class="token punctuation">{</span> tenantName<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span> email<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span> password<span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> tenant <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tenantsService<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>body<span class="token punctuation">.</span>tenantName<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> user <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersService<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>
      tenant<span class="token punctuation">.</span>id<span class="token punctuation">,</span>
      body<span class="token punctuation">.</span>email<span class="token punctuation">,</span>
      body<span class="token punctuation">.</span>password<span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">return</span> <span class="token punctuation">{</span>
      tenant<span class="token punctuation">:</span> <span class="token punctuation">{</span> id<span class="token punctuation">:</span> tenant<span class="token punctuation">.</span>id<span class="token punctuation">,</span> name<span class="token punctuation">:</span> tenant<span class="token punctuation">.</span>name <span class="token punctuation">}</span><span class="token punctuation">,</span>
      user<span class="token punctuation">:</span> <span class="token punctuation">{</span> id<span class="token punctuation">:</span> user<span class="token punctuation">.</span>id<span class="token punctuation">,</span> email<span class="token punctuation">:</span> user<span class="token punctuation">.</span>email <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Update the <code>TenantsModule</code> to import <code>UsersModule</code>:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TypeOrmModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TenantsController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tenants.controller'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> TenantsService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tenants.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Tenant <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./entities/tenant.entity'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> UsersModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../users/users.module'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>TypeOrmModule<span class="token punctuation">.</span><span class="token function">forFeature</span><span class="token punctuation">(</span><span class="token punctuation">[</span>Tenant<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span> UsersModule<span class="token punctuation">]</span><span class="token punctuation">,</span>
  controllers<span class="token punctuation">:</span> <span class="token punctuation">[</span>TenantsController<span class="token punctuation">]</span><span class="token punctuation">,</span>
  providers<span class="token punctuation">:</span> <span class="token punctuation">[</span>TenantsService<span class="token punctuation">]</span><span class="token punctuation">,</span>
  exports<span class="token punctuation">:</span> <span class="token punctuation">[</span>TenantsService<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TenantsModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h2 id="authentication-with-jwt">Authentication with JWT</h2><p>Our JWT tokens need to include the tenant ID so the middleware can extract it.</p><h3 id="jwt-strategy">JWT Strategy</h3><p>Create the <code>src/auth/strategies/jwt.strategy.ts</code> file and add the following to it:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> PassportStrategy <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/passport'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ExtractJwt<span class="token punctuation">,</span> Strategy <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'passport-jwt'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">JwtStrategy</span> <span class="token keyword">extends</span> <span class="token class-name">PassportStrategy</span><span class="token punctuation">(</span>Strategy<span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>configService<span class="token punctuation">:</span> ConfigService<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">super</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      jwtFromRequest<span class="token punctuation">:</span> ExtractJwt<span class="token punctuation">.</span><span class="token function">fromAuthHeaderAsBearerToken</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
      ignoreExpiration<span class="token punctuation">:</span> <span class="token keyword">false</span><span class="token punctuation">,</span>
      secretOrKey<span class="token punctuation">:</span> configService<span class="token punctuation">.</span><span class="token function">getOrThrow</span><span class="token punctuation">(</span><span class="token string">'JWT_SECRET'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">validate</span><span class="token punctuation">(</span>payload<span class="token punctuation">:</span> <span class="token keyword">any</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token punctuation">{</span>
      userId<span class="token punctuation">:</span> payload<span class="token punctuation">.</span>sub<span class="token punctuation">,</span>
      tenantId<span class="token punctuation">:</span> payload<span class="token punctuation">.</span>tenantId<span class="token punctuation">,</span>
      email<span class="token punctuation">:</span> payload<span class="token punctuation">.</span>email<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="jwt-auth-guard">JWT Auth Guard</h3><p>Create a <code>src/auth/guards/jwt-auth.guard.ts</code> file and add the following to it:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AuthGuard <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/passport'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">JwtAuthGuard</span> <span class="token keyword">extends</span> <span class="token class-name">AuthGuard</span><span class="token punctuation">(</span><span class="token string">'jwt'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h3 id="auth-service">Auth Service</h3><p>Update the <code>src/auth/auth.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable<span class="token punctuation">,</span> UnauthorizedException <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> JwtService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/jwt'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> UsersService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../users/users.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> bcrypt <span class="token keyword">from</span> <span class="token string">'bcrypt'</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AuthService</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    <span class="token keyword">private</span> usersService<span class="token punctuation">:</span> UsersService<span class="token punctuation">,</span>
    <span class="token keyword">private</span> jwtService<span class="token punctuation">:</span> JwtService<span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">login</span><span class="token punctuation">(</span>email<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> password<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> user <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersService<span class="token punctuation">.</span><span class="token function">findByEmail</span><span class="token punctuation">(</span>email<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>user<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">UnauthorizedException</span><span class="token punctuation">(</span><span class="token string">'Invalid credentials'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">const</span> isPasswordValid <span class="token operator">=</span> <span class="token keyword">await</span> bcrypt<span class="token punctuation">.</span><span class="token function">compare</span><span class="token punctuation">(</span>password<span class="token punctuation">,</span> user<span class="token punctuation">.</span>passwordHash<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>isPasswordValid<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">UnauthorizedException</span><span class="token punctuation">(</span><span class="token string">'Invalid credentials'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">const</span> payload <span class="token operator">=</span> <span class="token punctuation">{</span>
      sub<span class="token punctuation">:</span> user<span class="token punctuation">.</span>id<span class="token punctuation">,</span>
      tenantId<span class="token punctuation">:</span> user<span class="token punctuation">.</span>tenantId<span class="token punctuation">,</span>
      email<span class="token punctuation">:</span> user<span class="token punctuation">.</span>email<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>

    <span class="token keyword">return</span> <span class="token punctuation">{</span>
      access_token<span class="token punctuation">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>jwtService<span class="token punctuation">.</span><span class="token function">sign</span><span class="token punctuation">(</span>payload<span class="token punctuation">)</span><span class="token punctuation">,</span>
      user<span class="token punctuation">:</span> <span class="token punctuation">{</span>
        id<span class="token punctuation">:</span> user<span class="token punctuation">.</span>id<span class="token punctuation">,</span>
        email<span class="token punctuation">:</span> user<span class="token punctuation">.</span>email<span class="token punctuation">,</span>
        tenantId<span class="token punctuation">:</span> user<span class="token punctuation">.</span>tenantId<span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="auth-controller">Auth Controller</h3><p>Update the <code>src/auth/auth.controller.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Controller<span class="token punctuation">,</span> Post<span class="token punctuation">,</span> Body <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AuthService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./auth.service'</span><span class="token punctuation">;</span>

@<span class="token function">Controller</span><span class="token punctuation">(</span><span class="token string">'auth'</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AuthController</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> authService<span class="token punctuation">:</span> AuthService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  @<span class="token function">Post</span><span class="token punctuation">(</span><span class="token string">'login'</span><span class="token punctuation">)</span>
  <span class="token keyword">async</span> <span class="token function">login</span><span class="token punctuation">(</span>@<span class="token function">Body</span><span class="token punctuation">(</span><span class="token punctuation">)</span> body<span class="token punctuation">:</span> <span class="token punctuation">{</span> email<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span> password<span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>authService<span class="token punctuation">.</span><span class="token function">login</span><span class="token punctuation">(</span>body<span class="token punctuation">.</span>email<span class="token punctuation">,</span> body<span class="token punctuation">.</span>password<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="auth-module">Auth Module</h3><p>Update the <code>src/auth/auth.module.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> JwtModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/jwt'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> PassportModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/passport'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigModule<span class="token punctuation">,</span> ConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AuthController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./auth.controller'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AuthService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./auth.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> JwtStrategy <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./strategies/jwt.strategy'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> UsersModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../users/users.module'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>
    UsersModule<span class="token punctuation">,</span>
    PassportModule<span class="token punctuation">,</span>
    JwtModule<span class="token punctuation">.</span><span class="token function">registerAsync</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>ConfigModule<span class="token punctuation">]</span><span class="token punctuation">,</span>
      inject<span class="token punctuation">:</span> <span class="token punctuation">[</span>ConfigService<span class="token punctuation">]</span><span class="token punctuation">,</span>
      useFactory<span class="token punctuation">:</span> <span class="token punctuation">(</span>config<span class="token punctuation">:</span> ConfigService<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">(</span><span class="token punctuation">{</span>
        secret<span class="token punctuation">:</span> config<span class="token punctuation">.</span><span class="token function">getOrThrow</span><span class="token punctuation">(</span><span class="token string">'JWT_SECRET'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        signOptions<span class="token punctuation">:</span> <span class="token punctuation">{</span> expiresIn<span class="token punctuation">:</span> config<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'JWT_EXPIRATION'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">]</span><span class="token punctuation">,</span>
  controllers<span class="token punctuation">:</span> <span class="token punctuation">[</span>AuthController<span class="token punctuation">]</span><span class="token punctuation">,</span>
  providers<span class="token punctuation">:</span> <span class="token punctuation">[</span>AuthService<span class="token punctuation">,</span> JwtStrategy<span class="token punctuation">]</span><span class="token punctuation">,</span>
  exports<span class="token punctuation">:</span> <span class="token punctuation">[</span>JwtModule<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AuthModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h2 id="building-the-tasks-api">Building the Tasks API</h2><p>Now we can build the actual task management endpoints.</p><p>Update the <code>src/tasks/tasks.controller.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span>
    Controller<span class="token punctuation">,</span>
    Get<span class="token punctuation">,</span>
    Post<span class="token punctuation">,</span>
    Patch<span class="token punctuation">,</span>
    Delete<span class="token punctuation">,</span>
    Body<span class="token punctuation">,</span>
    Param<span class="token punctuation">,</span>
    UseGuards<span class="token punctuation">,</span>
    UseInterceptors<span class="token punctuation">,</span>
  <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
  <span class="token keyword">import</span> <span class="token punctuation">{</span> TasksService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./tasks.service'</span><span class="token punctuation">;</span>
  <span class="token keyword">import</span> <span class="token punctuation">{</span> JwtAuthGuard <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../auth/guards/jwt-auth.guard'</span><span class="token punctuation">;</span>
  <span class="token keyword">import</span> <span class="token punctuation">{</span> TenantContextInterceptor <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../database/tenant-context.interceptor'</span><span class="token punctuation">;</span>
  
  @<span class="token function">Controller</span><span class="token punctuation">(</span><span class="token string">'tasks'</span><span class="token punctuation">)</span>
  @<span class="token function">UseGuards</span><span class="token punctuation">(</span>JwtAuthGuard<span class="token punctuation">)</span>
  @<span class="token function">UseInterceptors</span><span class="token punctuation">(</span>TenantContextInterceptor<span class="token punctuation">)</span>
  <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">TasksController</span> <span class="token punctuation">{</span>
    <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> tasksService<span class="token punctuation">:</span> TasksService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
  
    @<span class="token function">Get</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token keyword">async</span> <span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tasksService<span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  
    @<span class="token function">Get</span><span class="token punctuation">(</span><span class="token string">':id'</span><span class="token punctuation">)</span>
    <span class="token keyword">async</span> <span class="token function">findOne</span><span class="token punctuation">(</span>@<span class="token function">Param</span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tasksService<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  
    @<span class="token function">Post</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token keyword">async</span> <span class="token function">create</span><span class="token punctuation">(</span>@<span class="token function">Body</span><span class="token punctuation">(</span><span class="token punctuation">)</span> body<span class="token punctuation">:</span> <span class="token punctuation">{</span> title<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span> description<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tasksService<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>body<span class="token punctuation">.</span>title<span class="token punctuation">,</span> body<span class="token punctuation">.</span>description<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  
    @<span class="token function">Patch</span><span class="token punctuation">(</span><span class="token string">':id'</span><span class="token punctuation">)</span>
    <span class="token keyword">async</span> <span class="token function">update</span><span class="token punctuation">(</span>
      @<span class="token function">Param</span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
      @<span class="token function">Body</span><span class="token punctuation">(</span><span class="token punctuation">)</span> body<span class="token punctuation">:</span> <span class="token punctuation">{</span> title<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span> description<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span> status<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tasksService<span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span>id<span class="token punctuation">,</span> body<span class="token punctuation">.</span>title<span class="token punctuation">,</span> body<span class="token punctuation">.</span>description<span class="token punctuation">,</span> body<span class="token punctuation">.</span>status<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    @<span class="token function">Delete</span><span class="token punctuation">(</span><span class="token string">':id'</span><span class="token punctuation">)</span> <span class="token keyword">async</span> <span class="token function">remove</span><span class="token punctuation">(</span>@<span class="token function">Param</span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>tasksService<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The <code>@UseGuards(JwtAuthGuard)</code> ensures all requests are authenticated. The <code>@UseInterceptors(TenantContextInterceptor)</code> wraps each request in a transaction with the tenant context set.</p><h2 id="testing-tenant-isolation">Testing Tenant Isolation</h2><p>Start the server:</p><pre class=" language-shell"><code class="prism  language-shell">npm run start:dev
</code></pre><h3 id="register-two-tenants">Register Two Tenants</h3><p>Register the first tenant (Company A):</p><pre class=" language-shell"><code class="prism  language-shell">curl -X POST http://localhost:3000/tenants/register \
  -H "Content-Type: application/json" \
  -d "{\"tenantName\": \"Company A\", \"email\": \"admin@companyA.com\", \"password\": \"password123\"}"
</code></pre><p>You should get a response like this:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/company-a-registration-response.png?sfvrsn=7d1f476c_2" title="Company A registration response" alt="Company A registration response" /></p><p>Register the second tenant (Company B):</p><pre class=" language-shell"><code class="prism  language-shell">curl -X POST http://localhost:3000/tenants/register \
  -H "Content-Type: application/json" \
  -d "{\"tenantName\": \"Company B\", \"email\": \"admin@companyB.com\", \"password\": \"password123\"}"
</code></pre><h3 id="login-and-get-tokens">Login and Get Tokens</h3><p>Log in as Company A:</p><pre class=" language-shell"><code class="prism  language-shell">curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d "{\"email\": \"admin@companyA.com\", \"password\": \"password123\"}"
</code></pre><p>You should get a response like this with an access token:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/company-a-logs-in-with-jwt-token.png?sfvrsn=221520cd_2" title="Company A logs in with JWT token" alt="Company A logs in with JWT token" /></p><p>Copy the token; we&rsquo;ll call it TOKEN_A. Do the same for Company B, and we&rsquo;ll call its token TOKEN_B.</p><pre class=" language-shell"><code class="prism  language-shell">curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d "{\"email\": \"admin@companyB.com\", \"password\": \"password123\"}"
</code></pre><h2 id="create-tasks">Create Tasks</h2><p>Create a task for Company A:</p><pre class=" language-shell"><code class="prism  language-shell">curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN_A" \
  -d "{\"title\": \"Company A Task\", \"description\": \"This belongs to Company A\"}"
</code></pre><p>Create a task for Company B:</p><pre class=" language-shell"><code class="prism  language-shell">curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN_B" \
  -d "{\"title\": \"Company B Task\", \"description\": \"This belongs to Company B\"}"
</code></pre><h2 id="verify-isolation">Verify Isolation</h2><p>List tasks as Company A:</p><pre class=" language-shell"><code class="prism  language-shell">curl http://localhost:3000/tasks \
  -H "Authorization: Bearer TOKEN_A"
</code></pre><p>You should only see Company A&rsquo;s task as seen below:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/company-a-task-list.png?sfvrsn=fbab1757_2" title="Company A’s task list" alt="Company A’s task list" /></p><p>List tasks as Company B:</p><pre class=" language-shell"><code class="prism  language-shell">curl http://localhost:3000/tasks \
  -H "Authorization: Bearer TOKEN_B"
</code></pre><p>You should only see Company B&rsquo;s task. This confirms that RLS is working, as each tenant only sees their own data.</p><h3 id="test-direct-id-access">Test Direct ID Access</h3><p>Now try to access Company A&rsquo;s task using Company B&rsquo;s token. First, get Company A&rsquo;s task ID from the previous response, then:</p><pre class=" language-shell"><code class="prism  language-shell">curl http://localhost:3000/tasks/ed396863-255f-49b4-a4c8-906f71465c9e \
  -H "Authorization: Bearer TOKEN_B"
</code></pre><p>You should get a 404 Not Found error like this:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/company-b-blocked.png?sfvrsn=8655cd8e_2" title="Company B blocked from accessing Company A&#39;s task" alt="Company B blocked from accessing Company A&#39;s task" /></p><p>Even though the task exists in the database, RLS prevents Company B from seeing it. This is the power of database-level isolation. Even if our application code has a bug, the database ensures tenants can&rsquo;t access each other&rsquo;s data.</p><h3 id="verify-company-a-can-access-its-own-task">Verify Company A Can Access Its Own Task</h3><p>Confirm the task exists and Company A can access it:</p><pre class=" language-shell"><code class="prism  language-shell">curl http://localhost:3000/tasks/ed396863-255f-49b4-a4c8-906f71465c9e \
  -H "Authorization: Bearer TOKEN_A"
</code></pre><p>You should get the full task details as shown below:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/company-a-can-access-their-task.png?sfvrsn=16fe441f_2" title="Company A can access their task successfully" alt="Company A can access their task successfully" /></p><h2 id="conclusion">Conclusion</h2><p>Now you can build a multi-tenant SaaS API using NestJS and Postgres Row-Level Security. We&rsquo;ve covered how to set up RLS policies at the database level, extract tenant context from JWT tokens, use TypeORM with transaction-scoped session variables, and verify that data isolation works even against direct ID access attempts.</p><p>This approach provides strong security guarantees without complex application logic. The database enforces isolation automatically, reducing the risk of accidentally exposing data across tenants.</p><p>Possible next steps include adding tenant-specific rate limiting, implementing admin endpoints for cross-tenant reporting (using <code>SECURITY DEFINER</code> functions to bypass RLS safely), or exploring performance optimizations for high-scale scenarios with connection pooling strategies.</p><hr /><p>&nbsp;</p><p>Read more:&nbsp;<a href="https://www.telerik.com/blogs/how-build-semantic-search-documentation-nestjs-qdrant-xenova" target="_blank">How to Build Semantic Search for Documentation with NestJS, Qdrant and Xenova</a></p><img src="https://feeds.telerik.com/link/10827/17341162.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:1feea2d1-2fa0-48fa-a5fa-f1d611bdf2d5</id>
    <title type="text">Streaming Server Events with SSE and Blazor</title>
    <summary type="text">Understand the SSE standard, the .NET 10  changes to simplify SSE endpoints and how to add real-time events to your Blazor clients.</summary>
    <published>2026-05-12T13:18:03Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Héctor Pérez </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17339193/streaming-server-events-sse-blazor"/>
    <content type="text"><![CDATA[<p><span class="featured">Understand the SSE standard, the .NET 10 changes to simplify SSE endpoints and how to add real-time events to your Blazor clients.</span></p><p>In the world of web development, there are scenarios where you need to receive real-time information, such as notifications about an event, server metrics or live logs. One possible solution to achieve this is the use of Server-Sent Events (SSE). With the arrival of .NET 10, implementing this type of solution has become much easier, so let&rsquo;s see how to integrate this web standard into your projects.</p><h2 id="what-are-server-sent-events">What Are Server-Sent Events?</h2><p>The <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events">Server-Sent Events (SSE)</a> are not a new technology. Their origins date back to around 2006, and they are a web standard that allows data to be sent to clients continuously using a persistent HTTP connection.</p><p>If you wonder the difference compared to other similar technologies, we can make a comparison:</p><ul><li><p><strong>SSE vs. Polling</strong>: In the case of polling, the direction goes from the client to the server, using the HTTP protocol. We can see it as a client asking the server if there are updates at certain intervals. It involves low implementation complexity.</p></li><li><p><strong>SSE vs. WebSockets</strong>: With WebSockets there is bidirectional communication. It involves high complexity and is ideal for scenarios like video games, chats, etc. It works over the WS protocol.</p></li><li><p><strong>SSE vs. SignalR</strong>: SignalR also establishes bidirectional communication, with medium implementation complexity. It allows the use of binary protocols, which makes it a very good option for scalable enterprise apps. It uses variable protocols.</p></li></ul><p>Analyzing the above, we can conclude that SSE is ideal when the data flow is unidirectional, you need it to work over the HTTP protocol and it must be easy to implement.</p><h2 id="how-does-the-sse-protocol-work">How Does the SSE Protocol Work?</h2><p>The workings behind the scenes of the SSE protocol, in broad terms, are as follows: a server SSE endpoint needs to send an HTTP response of the type <code>Content-Type: text/event-stream</code>. The response looks similar to the following:</p><pre class=" language-json"><code class="prism  language-json">event<span class="token punctuation">:</span> app<span class="token operator">-</span>event
data<span class="token punctuation">:</span> <span class="token punctuation">{</span><span class="token string">"id"</span><span class="token punctuation">:</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token string">"time"</span><span class="token punctuation">:</span><span class="token string">"10:30:45"</span><span class="token punctuation">,</span><span class="token string">"level"</span><span class="token punctuation">:</span><span class="token string">"Info"</span><span class="token punctuation">,</span><span class="token string">"source"</span><span class="token punctuation">:</span><span class="token string">"OrderService"</span><span class="token punctuation">,</span><span class="token string">"message"</span><span class="token punctuation">:</span><span class="token string">"Order #1234 placed successfully"</span><span class="token punctuation">}</span>
</code></pre><p>In the code above, there are some fields we should pay attention to:</p><ul><li><code>event</code>: Specifies the name of the event</li><li><code>data</code>: Is the content of the message</li><li><code>id</code>: Identifier used to perform a reconnection if needed</li></ul><h2 id="whats-new-in-.net-10-for-sse">What&rsquo;s New in .NET 10 for SSE</h2><p>The most important update in .NET 10 for working with SSE is that the ability to return a <code>ServerSentEvents</code> using the API <code>TypedResults.ServerSentEvents</code> has been implemented. This means that the method <code>TypedResults.ServerSentEvents&lt;T&gt;()</code> allows converting any <code>IAsyncEnumerable&lt;T&gt;</code> into a formatted SSE stream without needing to do anything else. Tasks like JSON serialization, HTTP headers, connection closing, etc. are handled automatically.</p><p>To better understand how it works, let&rsquo;s create a project using the SSE standard with ASP.NET 10.</p><h2 id="building-an-sse-project-using-asp.net-10">Building an SSE Project Using ASP.NET 10</h2><p>To practice the theoretical concepts covered so far, we&rsquo;ll create a page that shows a simulation of receiving events from backend services in real time. Using a Progress Telerik UI for <a target="_blank" href="https://www.telerik.com/blazor-ui/grid">Blazor Grid</a>, we&rsquo;ll perform tasks like filtering, grouping and analysis quickly.</p><h3 id="creating-and-configuring-the-project">Creating and Configuring the Project</h3><p>The first thing we&rsquo;ll do is create a project using the <strong>Blazor Web App</strong> template, selecting an <strong>Interactive render mode</strong> of <strong>Server</strong> and <strong>Interactivity location</strong> of <strong>Global</strong>. Next, we&rsquo;ll follow the official installation guide for <a target="_blank" href="https://www.telerik.com/blazor-ui">Telerik UI for Blazor</a> to configure the project and be able to use the Telerik components.</p><h3 id="creating-an-eventbroadcaster">Creating an EventBroadcaster</h3><p>For our project, we&rsquo;ll create an event broadcaster, which in simple terms will be a bus that SSE clients can connect to to consume events, while backend services will use it to publish events. First, we&rsquo;ll create a record that represents a system event:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">namespace</span> SSEDemo<span class="token punctuation">.</span>Services
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> record <span class="token function">AppEvent</span><span class="token punctuation">(</span><span class="token keyword">int</span> Id<span class="token punctuation">,</span> <span class="token keyword">string</span> Time<span class="token punctuation">,</span> <span class="token keyword">string</span> Level<span class="token punctuation">,</span> <span class="token keyword">string</span> Source<span class="token punctuation">,</span> <span class="token keyword">string</span> Message<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>Next, we will create the class <code>EventBroadcaster</code> which will have the following definition:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">EventBroadcaster</span>
<span class="token punctuation">{</span>
    <span class="token comment">//1.</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> Channel<span class="token operator">&lt;</span>AppEvent<span class="token operator">&gt;</span> _channel <span class="token operator">=</span> Channel<span class="token punctuation">.</span><span class="token generic-method function">CreateBounded<span class="token punctuation">&lt;</span>AppEvent<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>
    <span class="token keyword">new</span> <span class="token class-name">BoundedChannelOptions</span><span class="token punctuation">(</span><span class="token number">100</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> FullMode <span class="token operator">=</span> BoundedChannelFullMode<span class="token punctuation">.</span>DropOldest <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">private</span> <span class="token keyword">int</span> _nextId<span class="token punctuation">;</span>

    <span class="token comment">// 2.</span>
    <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">Publish</span><span class="token punctuation">(</span><span class="token keyword">string</span> level<span class="token punctuation">,</span> <span class="token keyword">string</span> source<span class="token punctuation">,</span> <span class="token keyword">string</span> message<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">var</span> id <span class="token operator">=</span> Interlocked<span class="token punctuation">.</span><span class="token function">Increment</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _nextId<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">var</span> evt <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AppEvent</span><span class="token punctuation">(</span>id<span class="token punctuation">,</span> DateTime<span class="token punctuation">.</span>Now<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"HH:mm:ss"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> level<span class="token punctuation">,</span> source<span class="token punctuation">,</span> message<span class="token punctuation">)</span><span class="token punctuation">;</span>
        _channel<span class="token punctuation">.</span>Writer<span class="token punctuation">.</span><span class="token function">TryWrite</span><span class="token punctuation">(</span>evt<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token comment">// 3.</span>
    <span class="token keyword">public</span> <span class="token keyword">async</span> IAsyncEnumerable<span class="token operator">&lt;</span>AppEvent<span class="token operator">&gt;</span> <span class="token function">Subscribe</span><span class="token punctuation">(</span>
        <span class="token punctuation">[</span>System<span class="token punctuation">.</span>Runtime<span class="token punctuation">.</span>CompilerServices<span class="token punctuation">.</span>EnumeratorCancellation<span class="token punctuation">]</span> CancellationToken ct<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span>ct<span class="token punctuation">.</span>IsCancellationRequested<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">bool</span> hasData<span class="token punctuation">;</span>
            <span class="token keyword">try</span>
            <span class="token punctuation">{</span>
                hasData <span class="token operator">=</span> <span class="token keyword">await</span> _channel<span class="token punctuation">.</span>Reader<span class="token punctuation">.</span><span class="token function">WaitToReadAsync</span><span class="token punctuation">(</span>ct<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
            <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">OperationCanceledException</span><span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                <span class="token keyword">yield</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>

            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>hasData<span class="token punctuation">)</span> <span class="token keyword">yield</span> <span class="token keyword">break</span><span class="token punctuation">;</span>

            <span class="token keyword">while</span> <span class="token punctuation">(</span>_channel<span class="token punctuation">.</span>Reader<span class="token punctuation">.</span><span class="token function">TryRead</span><span class="token punctuation">(</span><span class="token keyword">out</span> <span class="token keyword">var</span> evt<span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                <span class="token keyword">yield</span> <span class="token keyword">return</span> evt<span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>In the code above, we have the following sections:</p><ol><li><p>Sets up the channel where events will be passed, with a queue limit of 100 messages. <code>FullMode = BoundedChannelFullMode.DropOldest</code> allows that if the queue fills up and a new message arrives, the oldest one is removed to make room for the new message.</p></li><li><p>The method <code>Publish</code> is the one that will be used to emit events. <code>Interlocked.Increment</code> allows generating sequential IDs in a safe manner, while <code>AppEvent</code> packages the received data together with the obtained id. Finally the method <code>TryWrite()</code> attempts to write the event to the channel asynchronously.</p></li><li><p>On the other hand, the method <code>Subscribe</code> returns a continuous stream of asynchronous data through the use of <code>IAsyncEnumerable&lt;AppEvent&gt;</code>. Inside its implementation an infinite loop is created that waits for data to be available in the channel. Once an event enters the channel, it is emitted to the consumer. This will happen until the <code>CancellationToken</code> called <code>ct</code> is canceled.</p></li></ol><h3 id="generating-test-events">Generating Test Events</h3><p>To test the bus defined above, we&rsquo;ll create a service that simulates the activity of multiple services:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">DemoEventGenerator</span><span class="token punctuation">(</span>EventBroadcaster broadcaster<span class="token punctuation">)</span> <span class="token punctuation">:</span> BackgroundService
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token keyword">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span> Levels <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"Info"</span><span class="token punctuation">,</span> <span class="token string">"Warning"</span><span class="token punctuation">,</span> <span class="token string">"Error"</span><span class="token punctuation">,</span> <span class="token string">"Success"</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token keyword">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span> Sources <span class="token operator">=</span>
        <span class="token punctuation">[</span><span class="token string">"OrderService"</span><span class="token punctuation">,</span> <span class="token string">"PaymentService"</span><span class="token punctuation">,</span> <span class="token string">"InventoryService"</span><span class="token punctuation">,</span> <span class="token string">"AuthService"</span><span class="token punctuation">,</span> <span class="token string">"ShippingService"</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token keyword">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span> Messages <span class="token operator">=</span>
    <span class="token punctuation">[</span>
        <span class="token punctuation">[</span><span class="token string">"Order #{0} placed successfully"</span><span class="token punctuation">,</span> <span class="token string">"New customer registered"</span><span class="token punctuation">,</span> <span class="token string">"Product viewed: SKU-{0}"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token punctuation">[</span><span class="token string">"High latency detected: {0}ms"</span><span class="token punctuation">,</span> <span class="token string">"Retry attempt #{0}"</span><span class="token punctuation">,</span> <span class="token string">"Queue depth above threshold"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token punctuation">[</span><span class="token string">"Payment failed for order #{0}"</span><span class="token punctuation">,</span> <span class="token string">"Database timeout after {0}ms"</span><span class="token punctuation">,</span> <span class="token string">"Service unreachable"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token punctuation">[</span><span class="token string">"Deployment completed v2.{0}"</span><span class="token punctuation">,</span> <span class="token string">"Health check passed"</span><span class="token punctuation">,</span> <span class="token string">"Cache refreshed ({0} items)"</span><span class="token punctuation">]</span>
    <span class="token punctuation">]</span><span class="token punctuation">;</span>

    <span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token keyword">async</span> Task <span class="token function">ExecuteAsync</span><span class="token punctuation">(</span>CancellationToken stoppingToken<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">var</span> rng <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span>stoppingToken<span class="token punctuation">.</span>IsCancellationRequested<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>            
            <span class="token keyword">await</span> Task<span class="token punctuation">.</span><span class="token function">Delay</span><span class="token punctuation">(</span>rng<span class="token punctuation">.</span><span class="token function">Next</span><span class="token punctuation">(</span><span class="token number">1500</span><span class="token punctuation">,</span> <span class="token number">4000</span><span class="token punctuation">)</span><span class="token punctuation">,</span> stoppingToken<span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token keyword">var</span> levelIdx <span class="token operator">=</span> rng<span class="token punctuation">.</span><span class="token function">Next</span><span class="token punctuation">(</span>Levels<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">var</span> level <span class="token operator">=</span> Levels<span class="token punctuation">[</span>levelIdx<span class="token punctuation">]</span><span class="token punctuation">;</span>
            <span class="token keyword">var</span> source <span class="token operator">=</span> Sources<span class="token punctuation">[</span>rng<span class="token punctuation">.</span><span class="token function">Next</span><span class="token punctuation">(</span>Sources<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
            <span class="token keyword">var</span> templates <span class="token operator">=</span> Messages<span class="token punctuation">[</span>levelIdx<span class="token punctuation">]</span><span class="token punctuation">;</span>
            <span class="token keyword">var</span> message <span class="token operator">=</span> <span class="token keyword">string</span><span class="token punctuation">.</span><span class="token function">Format</span><span class="token punctuation">(</span>templates<span class="token punctuation">[</span>rng<span class="token punctuation">.</span><span class="token function">Next</span><span class="token punctuation">(</span>templates<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span> rng<span class="token punctuation">.</span><span class="token function">Next</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">,</span> <span class="token number">9999</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

            broadcaster<span class="token punctuation">.</span><span class="token function">Publish</span><span class="token punctuation">(</span>level<span class="token punctuation">,</span> source<span class="token punctuation">,</span> message<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The previous class is a random event generator, taking a random value from the arrays <code>Levels</code>, <code>Sources</code> and <code>Messages</code>. In addition, through the parameter <code>broadcaster</code>, the new event is published to the bus.</p><h3 id="creating-the-sse-endpoint">Creating the SSE Endpoint</h3><p>Now it&rsquo;s time to create the SSE endpoint, which will consume the shared <code>EventBroadcaster</code>, with the purpose of creating the Event Feed so clients can connect to receive events:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">SseEndpoints</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">MapSseEndpoints</span><span class="token punctuation">(</span><span class="token keyword">this</span> WebApplication app<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>        
        app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/sse/events"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>EventBroadcaster broadcaster<span class="token punctuation">,</span> CancellationToken ct<span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span>
            TypedResults<span class="token punctuation">.</span><span class="token function">ServerSentEvents</span><span class="token punctuation">(</span>broadcaster<span class="token punctuation">.</span><span class="token function">Subscribe</span><span class="token punctuation">(</span>ct<span class="token punctuation">)</span><span class="token punctuation">,</span> eventType<span class="token punctuation">:</span> <span class="token string">"app-event"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>In the code above you can notice how <code>EventBroadcaster</code> is injected as a parameter of <code>MapGet</code>. Likewise, <code>TypedResults.ServerSentEvent</code> is executed, which serializes the information and sends the correct SSE format to clients.</p><h3 id="registering-services-in-program.cs">Registering Services in Program.cs</h3><p>For everything to work as expected, we must register in <code>Program.cs</code> the different instances that will interact in the Blazor application. This means creating a singleton instance of <code>EventBroadcaster</code>, so that all systems have access to the same bus. Similarly, we will register <code>DemoEventGenerator</code> as a background service, through the method <code>AddHostedService</code>. This will allow simulated events to be generated in the background continuously:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">var</span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method function">AddSingleton<span class="token punctuation">&lt;</span>EventBroadcaster<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method function">AddHostedService<span class="token punctuation">&lt;</span>DemoEventGenerator<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">var</span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>

app<span class="token punctuation">.</span><span class="token function">MapSseEndpoints</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>In the previous code, in addition to registering the services, <code>MapSseEndpoints</code> is invoked to enable SSE communication in the project.</p><h3 id="creating-the-blazor-application">Creating the Blazor Application</h3><p>Once we have the application infrastructure ready, it&rsquo;s time to move on to the UI part. At this point, let&rsquo;s start by creating a JavaScript client, because the standard requires using the browser&rsquo;s <code>EventSource</code> API. Since the project is configured as <code>InteractiveServer</code>, we cannot add inline <code>script</code> tags. To work around this, I will add a new file inside the <code>wwwroot/js</code> folder called <code>sse-demos.js</code>:</p><pre class=" language-javascript"><code class="prism  language-javascript"><span class="token comment">//1.</span>
<span class="token keyword">let</span> eventFeedSource <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>

<span class="token number">2</span><span class="token punctuation">.</span>
<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">start</span><span class="token punctuation">(</span>dotNetRef<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token comment">// 3.</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>eventFeedSource<span class="token punctuation">)</span> eventFeedSource<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">//4.</span>
    <span class="token keyword">const</span> src <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">EventSource</span><span class="token punctuation">(</span><span class="token string">'/sse/events'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// 5.</span>
    src<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'app-event'</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        dotNetRef<span class="token punctuation">.</span><span class="token function">invokeMethodAsync</span><span class="token punctuation">(</span><span class="token string">'OnEventReceived'</span><span class="token punctuation">,</span> JSON<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>data<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// 6.</span>
    src<span class="token punctuation">.</span><span class="token function-variable function">onopen</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> dotNetRef<span class="token punctuation">.</span><span class="token function">invokeMethodAsync</span><span class="token punctuation">(</span><span class="token string">'OnConnectionChanged'</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
    src<span class="token punctuation">.</span><span class="token function-variable function">onerror</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> dotNetRef<span class="token punctuation">.</span><span class="token function">invokeMethodAsync</span><span class="token punctuation">(</span><span class="token string">'OnConnectionChanged'</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>

    eventFeedSource <span class="token operator">=</span> src<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">//7.</span>
<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>eventFeedSource<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        eventFeedSource<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        eventFeedSource <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The previous code does the following:</p><ol><li>A variable <code>eventFeedSource</code> is created to keep information about whether there is an active connection with the server.</li><li>A function named <code>start</code> should be called from C# code when you want to start receiving information&hellip;</li><li>It checks whether there is an open connection, in which case it is closed.</li><li>It tells the browser to connect to <code>/sse/events</code> and to listen for all events that the server sends.</li><li>It filters events named <code>app-event</code>.</li><li>We subscribe to the events <code>onopen</code> and <code>onerror</code>. Each will notify the method <code>OnConnectionChanged</code> about a change in the connection, which will allow showing a different state in the Blazor UI.</li><li>The function <code>stop</code> should be invoked to clean up memory when we want to close the connection.</li></ol><p>With the JS functions ready, the next step is to create the Blazor component. In this new component we will use a <a target="_blank" href="https://www.telerik.com/blazor-ui/grid">Blazor Data Grid</a> type component, because it is a highly configurable component that has built-in options to filter, group, etc., ideal for quickly obtaining information about events in the different systems.</p><p>To do the above, we will create a component called <code>EventFeed.razor</code>, which looks as follows:</p><pre class=" language-xml"><code class="prism  language-xml">@page "/events"
@rendermode InteractiveServer
@inject IJSRuntime JS
@implements IAsyncDisposable

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PageTitle</span><span class="token punctuation">&gt;</span></span>Live Event Feed<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PageTitle</span><span class="token punctuation">&gt;</span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mb-4<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Live Event Feed<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">&gt;</span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card shadow-sm<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-header d-flex align-items-center justify-content-between py-2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>d-flex align-items-center gap-3<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            @if (isStreaming)
            {
                &lt;span class="badge rounded-pill @(isConnected ? "bg-success" : "bg-secondary") fs-6"&gt;
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>me-1<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>⬤<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span>@(isConnected ? "Connected" : "Disconnected")
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span>
            }
            else
            {
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>badge rounded-pill bg-warning text-dark fs-6<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>me-1<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>⏸<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span>Paused
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span>
            }
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>text-muted<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                Events received: <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>strong</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>text-dark<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>@totalEventsReceived<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>strong</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>d-flex gap-2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikButton</span> <span class="token attr-name">OnClick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>ToggleStreaming<span class="token punctuation">"</span></span>
                           <span class="token attr-name">ThemeColor</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@(isStreaming ? ThemeConstants.Button.ThemeColor.Primary : ThemeConstants.Button.ThemeColor.Success)<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                @(isStreaming ? "⏸ Pause" : "▶ Resume")
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TelerikButton</span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikButton</span> <span class="token attr-name">OnClick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>ClearGrid<span class="token punctuation">"</span></span> <span class="token attr-name">ThemeColor</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@ThemeConstants.Button.ThemeColor.Light<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span> Clear<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TelerikButton</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>card-body p-0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TelerikGrid</span> <span class="token attr-name">Data</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@events<span class="token punctuation">"</span></span>
                     <span class="token attr-name">Height</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>560px<span class="token punctuation">"</span></span>
                     <span class="token attr-name">Sortable</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span>
                     <span class="token attr-name">Resizable</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span>
                     <span class="token attr-name">Reorderable</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span>
                     <span class="token attr-name">Groupable</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span>                     
                     <span class="token attr-name">ShowColumnMenu</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>GridColumns</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>GridColumn</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@nameof(AppEvent.Time)<span class="token punctuation">"</span></span> <span class="token attr-name">Title</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Time<span class="token punctuation">"</span></span> <span class="token attr-name">Width</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>110px<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>GridColumn</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@nameof(AppEvent.Level)<span class="token punctuation">"</span></span> <span class="token attr-name">Title</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Level<span class="token punctuation">"</span></span> <span class="token attr-name">Width</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>120px<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Template</span><span class="token punctuation">&gt;</span></span>
                        @{
                            var item = (AppEvent)context;
                        }
                        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>badge rounded-pill @GetBadgeClass(item.Level) px-3 py-2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>@item.Level<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Template</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>GridColumn</span><span class="token punctuation">&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>GridColumn</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@nameof(AppEvent.Source)<span class="token punctuation">"</span></span> <span class="token attr-name">Title</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Source<span class="token punctuation">"</span></span> <span class="token attr-name">Width</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>140px<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>GridColumn</span> <span class="token attr-name">Field</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@nameof(AppEvent.Message)<span class="token punctuation">"</span></span> <span class="token attr-name">Title</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Message<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>GridColumns</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TelerikGrid</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>

@code {
    private List<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>AppEvent</span><span class="token punctuation">&gt;</span></span> events = new();
    private bool isConnected;
    private bool isStreaming = true;
    private int totalEventsReceived;
    private DotNetObjectReference<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>EventFeed</span><span class="token punctuation">&gt;</span></span>? dotNetRef;
    private IJSObjectReference? jsModule;
    private bool _jsInitialized;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            dotNetRef = DotNetObjectReference.Create(this);
                       
            jsModule = await JS.InvokeAsync<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IJSObjectReference</span><span class="token punctuation">&gt;</span></span>("import", "./js/sse-demos.js");
                        
            await jsModule.InvokeVoidAsync("start", dotNetRef);
            
            _jsInitialized = true;
        }
    }

    [JSInvokable]
    public void OnEventReceived(AppEvent appEvent)
    {
        totalEventsReceived++;
        var updated = new List<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>AppEvent</span><span class="token punctuation">&gt;</span></span>(events);
        updated.Insert(0, appEvent);
        if (updated.Count &gt; 50)
            updated.RemoveRange(50, updated.Count - 50);
        events = updated;
        InvokeAsync(StateHasChanged);
    }

    [JSInvokable]
    public void OnConnectionChanged(bool connected)
    {
        isConnected = connected;
        InvokeAsync(StateHasChanged);
    }

    private async Task ToggleStreaming()
    {
        isStreaming = !isStreaming;
        
        if (jsModule is not null)
        {
            if (isStreaming)
                await jsModule.InvokeVoidAsync("start", dotNetRef);
            else
                await jsModule.InvokeVoidAsync("stop");
        }
    }

    private void ClearGrid()
    {
        events = new List<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>AppEvent</span><span class="token punctuation">&gt;</span></span>();
        StateHasChanged();
    }

    private string GetBadgeClass(string level) =&gt; level switch
    {
        "Info" =&gt; "bg-info text-dark",
        "Warning" =&gt; "bg-warning text-dark",
        "Error" =&gt; "bg-danger",
        "Success" =&gt; "bg-success",
        _ =&gt; "bg-secondary"
    };

    public async ValueTask DisposeAsync()
    {
        if (_jsInitialized &amp;&amp; jsModule is not null)
        {
            try
            {                
                await jsModule.InvokeVoidAsync("stop");
                                
                await jsModule.DisposeAsync();
            }
            catch
            {                
            }
        }
        dotNetRef?.Dispose();
    }

    public class AppEvent
    {
        public int Id { get; set; }
        public string Time { get; set; } = string.Empty;
        public string Level { get; set; } = string.Empty;
        public string Source { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
    }
}
</code></pre><p>In the previous code, there are some points to highlight:</p><ul><li><code>jsModule</code> dynamically loads the JS module.</li><li>The variable <code>dotNetRef</code> wraps the instance of the Blazor component that we will use inside the JS code.</li><li>The method <code>InvokeVoidAsync</code> is used both to start and to stop the event streaming.</li><li>The method <code>OnEventReceived</code> is invoked from the JS code each time an event is received. This allows updating the list <code>events</code> to show the new information in <code>TelerikGrid</code>.</li><li>The <code>OnConnectionChanged</code> method receives a connection status from the JS code to update the UI according to any change in the connection.</li></ul><p>With the new component ready, we can test the application, which looks like the following:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/telerik-datagrid-sse-event-feed.gif?sfvrsn=58cc913a_2" alt="Telerik DataGrid receiving live SSE event feed" /></p><p>With this, we verify that everything works correctly. Also, thanks to the Blazor DataGrid capabilities, we can perform operations such as monitoring only those high-severity events:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/timeline-grouped-events-view.png?sfvrsn=5ae1f10_2" alt="Timeline view of events grouped by day" /></p><p>With this we have a nice event viewer that monitors the status of multiple fictitious systems.</p><h2 id="conclusion">Conclusion</h2><p>Throughout this article you have learned about the SSE standard. You have also seen how changes introduced in .NET 10 help simplify the creation of SSE endpoints, enabling the creation of applications that send real-time events to clients. Now it&rsquo;s your turn to explore when you might use this web standard in your own projects. See you in the next article!</p><hr /><p>Try all this yourself with a free 30-day trial of Telerik UI for Blazor.</p><p><a href="https://www.telerik.com/try/ui-for-blazor" target="_blank" class="Btn">Try Now</a></p><img src="https://feeds.telerik.com/link/10827/17339193.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:5b92bc97-ce93-4caf-954e-4312b10c6ede</id>
    <title type="text">How to Build Semantic Search for Documentation with NestJS, Qdrant and Xenova</title>
    <summary type="text">In this post, we’ll build a semantic documentation search API that lets users ask natural-language questions instead of matching exact keywords.</summary>
    <published>2026-05-08T13:01:01Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Christian Nwamba </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17336853/how-build-semantic-search-documentation-nestjs-qdrant-xenova"/>
    <content type="text"><![CDATA[<p><span class="featured">In this post, we&rsquo;ll build a semantic documentation search API that lets users ask natural-language questions instead of matching exact keywords. Let&rsquo;s get started!</span></p><p>In this post, we&rsquo;ll build a semantic documentation search API that lets users ask natural-language questions instead of matching exact keywords. We&rsquo;ll use <a target="_blank" href="https://qdrant.tech/">Qdrant</a> as the vector database, <a target="_blank" href="https://www.npmjs.com/package/@xenova/transformers">Xenova/transformers</a> to generate local text embeddings and <a target="_blank" href="https://nestjs.com/">NestJS</a> as our API to tie everything together.</p><p>We will learn how to run Qdrant with <a target="_blank" href="https://www.docker.com/">Docker</a>, generate embeddings in <a target="_blank" href="https://nodejs.org/en">Node.js</a> and index docs as vectors with metadata in Qdrant. Our documentation API will provide a pure semantic search endpoint and a hybrid search endpoint that combines filters for an even more effective search.</p><h2 id="prerequisites">Prerequisites</h2><ul><li>Basic knowledge of NestJS and <a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a> </li><li>Basic knowledge of HTTP, RESTful APIs, and cURL</li><li>Node.js and Docker should be installed</li></ul><h2 id="how-semantic-search-works">How Semantic Search Works</h2><p>Semantic search focuses on meaning, not just words. It understands user intent and contextual meaning, then finds data with similar meaning rather than matching keywords. Semantic search solves this by converting text into vectors (arrays of numbers) that capture meaning, and then comparing these vectors to find related information.</p><p>For example, if our docs contain the phrase &ldquo;How to authenticate users using JWT&rdquo; and a user searches for &ldquo;login security setup,&rdquo; semantic search can infer they mean the same thing.</p><h2 id="what-is-qdrant">What Is Qdrant?</h2><p>Qdrant is a vector database built for speed. It stores vectors and handles nearest neighbor calculations quickly. Qdrant uses the HNSW algorithm (Hierarchical Navigable Small World) to find similar vectors and return results in milliseconds. We&rsquo;ll use the official Docker image to run it locally, which keeps our environment clean and makes the database easy to start and stop.</p><h2 id="what-is-xenova">What Is Xenova?</h2><p>Xenova lets you run machine learning models directly in Node.js. We&rsquo;ll use it through the <code>@xenova/transformers</code> package to generate embeddings locally. This means no API calls, no rate limits and our data doesn&rsquo;t leave our machine. The model downloads once (~23 MB) and caches locally for future use.</p><h2 id="project-setup">Project Setup</h2><p>First, create a NestJS project:</p><pre class=" language-shell"><code class="prism  language-shell">nest new semantic-search-api
cd semantic-search-api
</code></pre><p>Next, run the command below to install our dependencies:</p><pre class=" language-shell"><code class="prism  language-shell">npm install @nestjs/config @qdrant/js-client-rest @xenova/transformers uuid \
  &amp;&amp; npm install --save-dev @types/uuid
</code></pre><p>In our <code>install command</code>, <code>@nestjs/config</code> is used to import environment variables into our app, <code>@qdrant/js-client-rest</code> is the JavaScript client for interacting with the Qdrant vector database, <code>@xenova/transformers</code> is used to generate local text embeddings, and <code>uuid</code> is used to create unique identifiers for documents and embeddings.</p><h2 id="running-qdrant-with-docker">Running Qdrant with Docker</h2><p>Instead of installing Qdrant directly, we&rsquo;ll use Docker Compose to keep our environment clean. Create a <code>docker-compose.yml</code> file at the root of your project and paste the code below:</p><pre class=" language-yml"><code class="prism  language-yml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">'3.8'</span>

<span class="token key atrule">services</span><span class="token punctuation">:</span>
  <span class="token key atrule">qdrant</span><span class="token punctuation">:</span>
    <span class="token key atrule">image</span><span class="token punctuation">:</span> qdrant/qdrant<span class="token punctuation">:</span>latest
    <span class="token key atrule">container_name</span><span class="token punctuation">:</span> qdrant
    <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
    <span class="token key atrule">ports</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> "6333<span class="token punctuation">:</span>6333"  <span class="token comment"># REST API port</span>
    <span class="token key atrule">volumes</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> ./qdrant_storage<span class="token punctuation">:</span>/qdrant/storage
</code></pre><p>Start the database in the background:</p><pre class=" language-shell"><code class="prism  language-shell">docker-compose up -d
</code></pre><p>Next, create a <code>.env</code> file and paste your Qdrant connection settings and embedding configuration:</p><pre class=" language-js"><code class="prism  language-js">QDRANT_URL<span class="token operator">=</span>http<span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>localhost<span class="token punctuation">:</span><span class="token number">6333</span>
QDRANT_COLLECTION<span class="token operator">=</span>documentation
QDRANT_VECTOR_DIMENSION<span class="token operator">=</span><span class="token number">384</span>
HF_MODEL_CACHE<span class="token operator">=</span><span class="token punctuation">.</span><span class="token operator">/</span>models
</code></pre><p>The variables above configure Qdrant&rsquo;s URL and collection name, set the vector dimension to 384 (which matches our embedding model), and specify where Xenova caches the downloaded model.</p><p>Next, let&rsquo;s update the <code>app</code> module to import the <code>ConfigModule</code>, so that we can load environment variables in our app:</p><p>Update your <code>app.module.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>

@<span class="token function">Module</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  imports<span class="token punctuation">:</span> <span class="token punctuation">[</span>
    ConfigModule<span class="token punctuation">.</span><span class="token function">forRoot</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      isGlobal<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
      envFilePath<span class="token punctuation">:</span> <span class="token string">'.env'</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token comment">// ... we will add other modules here later</span>
  <span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
</code></pre><h2 id="project-structure">Project Structure</h2><p>Our project structure will look like this:</p><pre class=" language-js"><code class="prism  language-js">src<span class="token operator">/</span>
├── qdrant<span class="token operator">/</span>
│   ├── qdrant<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   └── qdrant<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
├── embeddings<span class="token operator">/</span>
│   ├── embeddings<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   └── embeddings<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
├── documents<span class="token operator">/</span>
│   ├── documents<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
│   ├── documents<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>ts
│   ├── document<span class="token operator">-</span>ingestion<span class="token operator">/</span>
│   │   ├── document<span class="token operator">-</span>ingestion<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
│   │   └── document<span class="token operator">-</span>ingestion<span class="token punctuation">.</span>service<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
│   └── document<span class="token operator">-</span>processor<span class="token operator">/</span>
│       ├── document<span class="token operator">-</span>processor<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
│       └── document<span class="token operator">-</span>processor<span class="token punctuation">.</span>service<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
└── search<span class="token operator">/</span>
    ├── search<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts
    ├── search<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts
    ├── search<span class="token punctuation">.</span>service<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts
    └── search<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>ts
</code></pre><p>Run the command below to generate the necessary files:</p><pre class=" language-shell"><code class="prism  language-shell">nest g module qdrant &amp;&amp; \
nest g service qdrant &amp;&amp; \
nest g module embeddings &amp;&amp; \
nest g service embeddings &amp;&amp; \
nest g module documents &amp;&amp; \
nest g service documents/document-processor &amp;&amp; \
nest g service documents/document-ingestion &amp;&amp; \
nest g controller documents &amp;&amp; \
nest g module search &amp;&amp; \
nest g service search &amp;&amp; \
nest g controller search
</code></pre><h2 id="choosing-an-embedding-model">Choosing an Embedding Model</h2><p>For this project, we&rsquo;ll be using Xenova/all-MiniLM-L6-v2 for embeddings. This model is great at producing sentence-level embeddings, which work well for semantic search over documentation. It is relatively small and fast, and this makes it practical to run in Node.js without requiring a GPU. It outputs fixed 384-dimensional vectors (arrays with a fixed length of 384), which match our Qdrant collection configuration.</p><p>The model runs completely locally. On first use, Xenova downloads and caches it, and every subsequent run uses the cached version.</p><h2 id="building-the-embedding-service">Building the Embedding Service</h2><p>Our <code>EmbeddingService</code> will be responsible for converting text into vectors. Open the <code>embeddings.service.ts</code> file and update it with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span>
  pipeline<span class="token punctuation">,</span>
  env<span class="token punctuation">,</span>
  FeatureExtractionPipeline<span class="token punctuation">,</span>
<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@xenova/transformers'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">type</span> EmbeddingVector <span class="token operator">=</span> <span class="token keyword">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">EmbeddingsService</span> <span class="token punctuation">{</span>
  <span class="token keyword">private</span> extractor<span class="token punctuation">:</span> FeatureExtractionPipeline <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
  <span class="token keyword">private</span> readonly DIMENSION<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>

  <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> readonly configService<span class="token punctuation">:</span> ConfigService<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> vectorDimensionEnv <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>configService<span class="token punctuation">.</span>getOrThrow<span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span><span class="token punctuation">(</span>
      <span class="token string">'QDRANT_VECTOR_DIMENSION'</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>DIMENSION <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>vectorDimensionEnv<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token keyword">async</span> <span class="token function">getExtractor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>FeatureExtractionPipeline<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token keyword">this</span><span class="token punctuation">.</span>extractor<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      env<span class="token punctuation">.</span>localModelPath <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>configService<span class="token punctuation">.</span>getOrThrow<span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span><span class="token punctuation">(</span>
        <span class="token string">'HF_MODEL_CACHE'</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token punctuation">;</span>

      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Loading embedding model (first time only, ~5s)...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">const</span> pipe <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">pipeline</span><span class="token punctuation">(</span>
        <span class="token string">'feature-extraction'</span><span class="token punctuation">,</span>
        <span class="token string">'Xenova/all-MiniLM-L6-v2'</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">this</span><span class="token punctuation">.</span>extractor <span class="token operator">=</span> pipe<span class="token punctuation">;</span>
      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Embedding model loaded.'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>extractor<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">embed</span><span class="token punctuation">(</span>text<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>EmbeddingVector<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> extractor <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getExtractor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> output <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">extractor</span><span class="token punctuation">(</span>text<span class="token punctuation">,</span> <span class="token punctuation">{</span>
      pooling<span class="token punctuation">:</span> <span class="token string">'mean'</span><span class="token punctuation">,</span>
      normalize<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">return</span> <span class="token keyword">Array</span><span class="token punctuation">.</span><span class="token keyword">from</span><span class="token punctuation">(</span>output<span class="token punctuation">.</span>data <span class="token keyword">as</span> Float32Array<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">embedBatch</span><span class="token punctuation">(</span>texts<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>EmbeddingVector<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> extractor <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getExtractor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> output <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">extractor</span><span class="token punctuation">(</span>texts<span class="token punctuation">,</span> <span class="token punctuation">{</span>
      pooling<span class="token punctuation">:</span> <span class="token string">'mean'</span><span class="token punctuation">,</span>
      normalize<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">Array</span><span class="token punctuation">.</span><span class="token keyword">from</span><span class="token punctuation">(</span>output<span class="token punctuation">.</span>data <span class="token keyword">as</span> Float32Array<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token keyword">Array</span><span class="token punctuation">.</span><span class="token keyword">from</span><span class="token punctuation">(</span><span class="token punctuation">{</span> length<span class="token punctuation">:</span> texts<span class="token punctuation">.</span>length <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>_<span class="token punctuation">,</span> i<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
      data<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span>i <span class="token operator">*</span> <span class="token keyword">this</span><span class="token punctuation">.</span>DIMENSION<span class="token punctuation">,</span> <span class="token punctuation">(</span>i <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token keyword">this</span><span class="token punctuation">.</span>DIMENSION<span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">warmup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span><span class="token keyword">void</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
      <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">embed</span><span class="token punctuation">(</span><span class="token string">'warmup'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Embedding model warmup completed.'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Embedding model warmup failed:'</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">throw</span> error<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The <code>extractor</code> property stores our loaded embedding model. It is initialized as <code>null</code> and loaded lazily on first use. This means the model only downloads when it is actually needed, rather than slowing down application startup.</p><p>The <code>getExtractor()</code> method loads and caches the model. First, we check if <code>this.extractor</code> already exists. If it does, we return it. If not, we set <code>env.localModelPath</code> to tell Xenova where to cache the downloaded model files, then call <code>pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2')</code> to download and load the model.</p><p>The <code>embed()</code> method calls the extractor with two options: <code>pooling: 'mean'</code>, which averages all token embeddings into a single vector, and <code>normalize: true</code>, which scales the vector to unit length (required for cosine similarity in Qdrant). The extractor returns a Float32Array, which we convert to a regular array using <code>Array.from()</code>.</p><p>For <code>embedBatch()</code>, we pass an array of texts to the extractor. The model returns a flattened array containing all vectors concatenated together. We split this back into individual vectors by slicing out chunks of 384 values (our vector dimension). The first text gets indices 0&ndash;383, the second gets 384&ndash;767 and so on.</p><p>The <code>warmup()</code> method runs a dummy embedding to preload the model, preventing the first real user request from experiencing a delay while the model loads. Be sure to export the <code>EmbeddingsService</code> in the <code>EmbeddingsModule</code>.</p><h2 id="building-the-qdrant-service">Building the Qdrant Service</h2><p>This service wraps the vector database and handles creating the collection as well as reading and writing vectors. Update the <code>qdrant.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> ConfigService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/config'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span>
  QdrantClient<span class="token punctuation">,</span>
  Schemas<span class="token punctuation">,</span>
<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@qdrant/js-client-rest'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">IQdrantPayload</span> <span class="token punctuation">{</span>
  title<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  category<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  url<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  text<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  chunkIndex<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
  <span class="token punctuation">[</span>key<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">]</span><span class="token punctuation">:</span> unknown<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">IQdrantPoint</span> <span class="token punctuation">{</span>
  id<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  vector<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
  payload<span class="token punctuation">:</span> IQdrantPayload<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">QdrantService</span> <span class="token punctuation">{</span>
  <span class="token keyword">private</span> readonly client<span class="token punctuation">:</span> QdrantClient<span class="token punctuation">;</span>
  <span class="token keyword">private</span> readonly vectorDimension<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
  <span class="token keyword">private</span> readonly collectionName<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>

  <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> readonly configService<span class="token punctuation">:</span> ConfigService<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>configService<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token string">'QDRANT_URL'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>url<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'QDRANT_URL is not set in environment'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">this</span><span class="token punctuation">.</span>collectionName <span class="token operator">=</span>
      <span class="token keyword">this</span><span class="token punctuation">.</span>configService<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token string">'QDRANT_COLLECTION'</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token string">'documentation'</span><span class="token punctuation">;</span>

    <span class="token keyword">const</span> vectorDimensionEnv <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>configService<span class="token punctuation">.</span>getOrThrow<span class="token operator">&lt;</span><span class="token keyword">string</span><span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token string">'QDRANT_VECTOR_DIMENSION'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>vectorDimension <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>vectorDimensionEnv<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">this</span><span class="token punctuation">.</span>client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">QdrantClient</span><span class="token punctuation">(</span><span class="token punctuation">{</span> url <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token function">getCollectionName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>collectionName<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">setupCollection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span><span class="token keyword">void</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> collections <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>client<span class="token punctuation">.</span><span class="token function">getCollections</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> exists <span class="token operator">=</span> collections<span class="token punctuation">.</span>collections<span class="token operator">?</span><span class="token punctuation">.</span><span class="token function">some</span><span class="token punctuation">(</span>
      c <span class="token operator">=&gt;</span> c<span class="token punctuation">.</span>name <span class="token operator">===</span> <span class="token keyword">this</span><span class="token punctuation">.</span>collectionName<span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span>exists<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`✓ Collection "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>collectionName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" already exists.`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">return</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>client<span class="token punctuation">.</span><span class="token function">createCollection</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>collectionName<span class="token punctuation">,</span> <span class="token punctuation">{</span>
      vectors<span class="token punctuation">:</span> <span class="token punctuation">{</span>
        size<span class="token punctuation">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>vectorDimension<span class="token punctuation">,</span>
        distance<span class="token punctuation">:</span> <span class="token string">'Cosine'</span><span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`✓ Created collection "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>collectionName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">".`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">upsertPoints</span><span class="token punctuation">(</span>points<span class="token punctuation">:</span> IQdrantPoint<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span><span class="token keyword">void</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>client<span class="token punctuation">.</span><span class="token function">upsert</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>collectionName<span class="token punctuation">,</span> <span class="token punctuation">{</span>
      wait<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
      points<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">search</span><span class="token punctuation">(</span>
    vector<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    limit<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">,</span>
    filter<span class="token operator">?</span><span class="token punctuation">:</span> Schemas\<span class="token punctuation">[</span><span class="token string">'SearchRequest'</span>\<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'filter'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>Schemas\<span class="token punctuation">[</span><span class="token string">'ScoredPoint'</span>\<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> params<span class="token punctuation">:</span> Schemas<span class="token punctuation">[</span><span class="token string">'SearchRequest'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">{</span>
      vector<span class="token punctuation">,</span>
      limit<span class="token punctuation">,</span>
      with_payload<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
      with_vector<span class="token punctuation">:</span> <span class="token keyword">false</span><span class="token punctuation">,</span>
      <span class="token operator">...</span><span class="token punctuation">(</span>filter <span class="token operator">&amp;&amp;</span> <span class="token punctuation">{</span> filter <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>

    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>client<span class="token punctuation">.</span><span class="token function">search</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>collectionName<span class="token punctuation">,</span> params<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>We define an interface for our vector points. Each point has an ID, a vector and a payload. The payload holds metadata such as the text content and URL.</p><p>Our constructor reads our environment variables and creates a Qdrant client. The <code>setupCollection()</code> method checks if our collection exists and creates it if it doesn&rsquo;t. We use Cosine distance, which is the standard for semantic similarity.</p><p>The <code>upsertPoints()</code> method saves vectors and their metadata to Qdrant. Finally, the <code>search()</code> method finds similar vectors. We request the payload but not the vector itself, since we only need the metadata for displaying results.</p><h2 id="document-chunking">Document Chunking</h2><h3 id="document-processing-service">Document Processing Service</h3><p>LLMs and vector databases work best with smaller chunks of text. When large text, such as a 10-page document, is embedded as a single vector, specific details get lost.</p><p>While our Xenova model has a safe upper bound limit of approximately 2,000 characters, for best search quality it is advised to embed text with a length of 400&ndash;600 characters. Therefore, we need to split documents into chunks; for this project, we&rsquo;ll aim for around 500 characters.</p><p>Our chunking strategy will aim for the maximum number of complete paragraphs we can fit within the 500-character limit. Then we&rsquo;ll start a new chunk with an overlap from the end of the previous chunk. The purpose of the overlap is to preserve context across chunk boundaries.</p><p>Update your <code>document-processor.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">IDocumentMetadata</span> <span class="token punctuation">{</span>
  title<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  category<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  url<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">IDocumentChunk</span> <span class="token punctuation">{</span>
  text<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  chunkIndex<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
  metadata<span class="token punctuation">:</span> IDocumentMetadata<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">DocumentProcessorService</span> <span class="token punctuation">{</span>
  <span class="token keyword">private</span> readonly CHUNK_SIZE <span class="token operator">=</span> <span class="token number">500</span><span class="token punctuation">;</span> <span class="token comment">// characters</span>
  <span class="token keyword">private</span> readonly OVERLAP <span class="token operator">=</span> <span class="token number">50</span><span class="token punctuation">;</span>     <span class="token comment">// characters</span>


  <span class="token function">chunkDocument</span><span class="token punctuation">(</span>
    content<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
    metadata<span class="token punctuation">:</span> IDocumentMetadata<span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">:</span> IDocumentChunk<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> chunks<span class="token punctuation">:</span> IDocumentChunk<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> paragraphs <span class="token operator">=</span> content
      <span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'\n\n'</span><span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>p <span class="token operator">=&gt;</span> p<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>p <span class="token operator">=&gt;</span> p<span class="token punctuation">.</span>length <span class="token operator">&gt;</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">let</span> currentChunk <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">;</span>
    <span class="token keyword">let</span> chunkIndex <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>

    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> paragraph <span class="token keyword">of</span> paragraphs<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">const</span> potentialChunk <span class="token operator">=</span> currentChunk
        <span class="token operator">?</span> <span class="token template-string"><span class="token string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>currentChunk<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">\n\n</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>paragraph<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">`</span></span>
        <span class="token punctuation">:</span> paragraph<span class="token punctuation">;</span>

      <span class="token keyword">if</span> <span class="token punctuation">(</span>potentialChunk<span class="token punctuation">.</span>length <span class="token operator">&gt;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>CHUNK_SIZE <span class="token operator">&amp;&amp;</span> currentChunk<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token comment">// Current chunk is full; emit it</span>
        chunks<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
          text<span class="token punctuation">:</span> currentChunk<span class="token punctuation">,</span>
          chunkIndex<span class="token punctuation">,</span>
          metadata<span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// Start new chunk with overlap (prefer complete sentence, fallback to word boundary)</span>
        <span class="token keyword">const</span> overlap <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">findOverlap</span><span class="token punctuation">(</span>currentChunk<span class="token punctuation">)</span><span class="token punctuation">;</span>
        currentChunk <span class="token operator">=</span> overlap <span class="token operator">+</span> <span class="token string">'\n\n'</span> <span class="token operator">+</span> paragraph<span class="token punctuation">;</span>
        chunkIndex<span class="token operator">++</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
        currentChunk <span class="token operator">=</span> potentialChunk<span class="token punctuation">;</span>
      <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>

    <span class="token comment">// Emit last chunk</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>currentChunk<span class="token punctuation">.</span>length <span class="token operator">&gt;</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      chunks<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
        text<span class="token punctuation">:</span> currentChunk<span class="token punctuation">,</span>
        chunkIndex<span class="token punctuation">,</span>
        metadata<span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">return</span> chunks<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">findOverlap</span><span class="token punctuation">(</span>text<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> searchWindow <span class="token operator">=</span> text<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token keyword">this</span><span class="token punctuation">.</span>OVERLAP <span class="token operator">*</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    
    <span class="token comment">// Try to find last complete sentence</span>
    <span class="token keyword">const</span> sentenceMatch <span class="token operator">=</span> searchWindow<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex">/[.!?]\s+([^.!?]+)$/</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>sentenceMatch<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> sentenceMatch<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    
    <span class="token comment">// Fallback: find word boundary near target overlap length</span>
    <span class="token keyword">const</span> tail <span class="token operator">=</span> text<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token keyword">this</span><span class="token punctuation">.</span>OVERLAP <span class="token operator">*</span> <span class="token number">1.5</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> wordMatch <span class="token operator">=</span> tail<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex">/\s+(\S+.*)$/</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>wordMatch<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> wordMatch<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    
    <span class="token comment">// Last resort: from last space</span>
    <span class="token keyword">const</span> lastSpace <span class="token operator">=</span> text<span class="token punctuation">.</span><span class="token function">lastIndexOf</span><span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">,</span> text<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token keyword">this</span><span class="token punctuation">.</span>OVERLAP<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> lastSpace <span class="token operator">!==</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">?</span> text<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span>lastSpace <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">:</span> text<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token keyword">this</span><span class="token punctuation">.</span>OVERLAP<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>In the code above, we set our chunk size to 500 characters with a 50-character overlap. The overlap helps preserve context across chunk boundaries.</p><p>The <code>chunkDocument()</code> method splits text by paragraph, then accumulates paragraphs until the next one would exceed our limit. It then saves the chunk and starts a new one with an overlap from the end of the chunk that was just saved.</p><p>The <code>findOverlap()</code> method tries to find a complete sentence for the overlap first. If that fails, it looks for a word boundary. This keeps the overlap readable rather than cutting words in half.</p><h3 id="document-ingestion-service">Document Ingestion Service</h3><p>This service processes raw documents, converts them to vectors and saves them in Qdrant. Update your <code>document-ingestion.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> v4 <span class="token keyword">as</span> uuidv4 <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'uuid'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> DocumentProcessorService<span class="token punctuation">,</span> IDocumentMetadata<span class="token punctuation">,</span> IDocumentChunk <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../document-processor/document-processor.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> EmbeddingsService<span class="token punctuation">,</span> EmbeddingVector <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../../embeddings/embeddings.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> QdrantService<span class="token punctuation">,</span> IQdrantPoint<span class="token punctuation">,</span> IQdrantPayload <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../../qdrant/qdrant.service'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">IRawDocument</span> <span class="token keyword">extends</span> <span class="token class-name">IDocumentMetadata</span> <span class="token punctuation">{</span>
  content<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">DocumentIngestionService</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    <span class="token keyword">private</span> readonly processor<span class="token punctuation">:</span> DocumentProcessorService<span class="token punctuation">,</span>
    <span class="token keyword">private</span> readonly embeddings<span class="token punctuation">:</span> EmbeddingsService<span class="token punctuation">,</span>
    <span class="token keyword">private</span> readonly qdrant<span class="token punctuation">:</span> QdrantService<span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token comment">/**
   * Ingest one or more raw documents:
   * - Chunk content into smaller overlapping pieces.
   * - Embed all chunk texts in a batch.
   * - Upsert points (vector + payload) into Qdrant.
   */</span>
  <span class="token keyword">async</span> <span class="token function">ingestDocuments</span><span class="token punctuation">(</span>docs<span class="token punctuation">:</span> IRawDocument<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span><span class="token punctuation">{</span>
    status<span class="token punctuation">:</span> <span class="token string">'ok'</span> <span class="token operator">|</span> <span class="token string">'error'</span><span class="token punctuation">;</span>
    documents<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
    totalChunks<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
    skipped<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
    error<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
      <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>docs<span class="token operator">?</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">'ok'</span><span class="token punctuation">,</span> documents<span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">,</span> totalChunks<span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">,</span> skipped<span class="token punctuation">:</span> <span class="token number">0</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>

      <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>qdrant<span class="token punctuation">.</span><span class="token function">setupCollection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

      <span class="token keyword">let</span> totalChunks <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
      <span class="token keyword">let</span> skipped <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>

      <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> doc <span class="token keyword">of</span> docs<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">ingestDocument</span><span class="token punctuation">(</span>doc<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>result<span class="token punctuation">.</span>success<span class="token punctuation">)</span> <span class="token punctuation">{</span>
          totalChunks <span class="token operator">+=</span> result<span class="token punctuation">.</span>chunks<span class="token punctuation">;</span>
        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
          skipped<span class="token operator">++</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
      <span class="token punctuation">}</span>

      <span class="token keyword">return</span> <span class="token punctuation">{</span>
        status<span class="token punctuation">:</span> <span class="token string">'ok'</span><span class="token punctuation">,</span>
        documents<span class="token punctuation">:</span> docs<span class="token punctuation">.</span>length <span class="token operator">-</span> skipped<span class="token punctuation">,</span>
        totalChunks<span class="token punctuation">,</span>
        skipped<span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Fatal error during ingestion:'</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">return</span> <span class="token punctuation">{</span>
        status<span class="token punctuation">:</span> <span class="token string">'error'</span><span class="token punctuation">,</span>
        documents<span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
        totalChunks<span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
        skipped<span class="token punctuation">:</span> docs<span class="token operator">?</span><span class="token punctuation">.</span>length <span class="token operator">?</span><span class="token operator">?</span> <span class="token number">0</span><span class="token punctuation">,</span>
        error<span class="token punctuation">:</span> error <span class="token keyword">instanceof</span> <span class="token class-name">Error</span> <span class="token operator">?</span> error<span class="token punctuation">.</span>message <span class="token punctuation">:</span> <span class="token string">'Unknown error'</span><span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token keyword">async</span> <span class="token function">ingestDocument</span><span class="token punctuation">(</span>doc<span class="token punctuation">:</span> IRawDocument<span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span><span class="token punctuation">{</span>
    success<span class="token punctuation">:</span> <span class="token keyword">boolean</span><span class="token punctuation">;</span>
    chunks<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
      <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>doc<span class="token punctuation">.</span>title <span class="token operator">||</span> <span class="token operator">!</span>doc<span class="token punctuation">.</span>content<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        console<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">'Skipping document, missing title or content'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span> success<span class="token punctuation">:</span> <span class="token keyword">false</span><span class="token punctuation">,</span> chunks<span class="token punctuation">:</span> <span class="token number">0</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>

      <span class="token keyword">const</span> metadata<span class="token punctuation">:</span> IDocumentMetadata <span class="token operator">=</span> <span class="token punctuation">{</span>
        title<span class="token punctuation">:</span> doc<span class="token punctuation">.</span>title<span class="token punctuation">,</span>
        category<span class="token punctuation">:</span> doc<span class="token punctuation">.</span>category <span class="token operator">||</span> <span class="token string">'uncategorized'</span><span class="token punctuation">,</span>
        url<span class="token punctuation">:</span> doc<span class="token punctuation">.</span>url <span class="token operator">||</span> <span class="token string">''</span><span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">;</span>

      <span class="token keyword">const</span> chunks <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>processor<span class="token punctuation">.</span><span class="token function">chunkDocument</span><span class="token punctuation">(</span>doc<span class="token punctuation">.</span>content<span class="token punctuation">,</span> metadata<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">if</span> <span class="token punctuation">(</span>chunks<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        console<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`Skipping "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>doc<span class="token punctuation">.</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" - produced no chunks`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span> success<span class="token punctuation">:</span> <span class="token keyword">false</span><span class="token punctuation">,</span> chunks<span class="token punctuation">:</span> <span class="token number">0</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>

      <span class="token keyword">const</span> vectors <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>embeddings<span class="token punctuation">.</span><span class="token function">embedBatch</span><span class="token punctuation">(</span>
        chunks<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>chunk <span class="token operator">=&gt;</span> chunk<span class="token punctuation">.</span>text<span class="token punctuation">)</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token punctuation">;</span>

      <span class="token keyword">if</span> <span class="token punctuation">(</span>vectors<span class="token punctuation">.</span>length <span class="token operator">!==</span> chunks<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>
          <span class="token template-string"><span class="token string">`Error ingesting "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>doc<span class="token punctuation">.</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">": expected </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>chunks<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> vectors, got </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>vectors<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">`</span></span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span> success<span class="token punctuation">:</span> <span class="token keyword">false</span><span class="token punctuation">,</span> chunks<span class="token punctuation">:</span> <span class="token number">0</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>

      <span class="token keyword">const</span> points <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">createQdrantPoints</span><span class="token punctuation">(</span>chunks<span class="token punctuation">,</span> vectors<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>qdrant<span class="token punctuation">.</span><span class="token function">upsertPoints</span><span class="token punctuation">(</span>points<span class="token punctuation">)</span><span class="token punctuation">;</span>

      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`✓ Ingested "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>doc<span class="token punctuation">.</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>chunks<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> chunks).`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">return</span> <span class="token punctuation">{</span> success<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span> chunks<span class="token punctuation">:</span> chunks<span class="token punctuation">.</span>length <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`Error ingesting "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>doc<span class="token punctuation">.</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">":`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">return</span> <span class="token punctuation">{</span> success<span class="token punctuation">:</span> <span class="token keyword">false</span><span class="token punctuation">,</span> chunks<span class="token punctuation">:</span> <span class="token number">0</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">createQdrantPoints</span><span class="token punctuation">(</span>
    chunks<span class="token punctuation">:</span> IDocumentChunk<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    vectors<span class="token punctuation">:</span> EmbeddingVector<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">:</span> IQdrantPoint<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> chunks<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span>chunk<span class="token punctuation">,</span> index<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">(</span><span class="token punctuation">{</span>
      id<span class="token punctuation">:</span> <span class="token function">uuidv4</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
      vector<span class="token punctuation">:</span> vectors<span class="token punctuation">[</span>index<span class="token punctuation">]</span><span class="token punctuation">,</span>
      payload<span class="token punctuation">:</span> <span class="token punctuation">{</span>
        title<span class="token punctuation">:</span> chunk<span class="token punctuation">.</span>metadata<span class="token punctuation">.</span>title<span class="token punctuation">,</span>
        category<span class="token punctuation">:</span> chunk<span class="token punctuation">.</span>metadata<span class="token punctuation">.</span>category<span class="token punctuation">,</span>
        url<span class="token punctuation">:</span> chunk<span class="token punctuation">.</span>metadata<span class="token punctuation">.</span>url<span class="token punctuation">,</span>
        text<span class="token punctuation">:</span> chunk<span class="token punctuation">.</span>text<span class="token punctuation">,</span>
        chunkIndex<span class="token punctuation">:</span> chunk<span class="token punctuation">.</span>chunkIndex<span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>In our constructor, we inject three services: the document processor for chunking, the embeddings service for generating vectors and the Qdrant service for database operations.</p><p>The <code>ingestDocuments()</code> method processes multiple documents at once. First, it confirms that the Qdrant collection is set up, then it processes each document individually. If one document fails, we still proceed with the others while tracking which documents were skipped and which were successful.</p><p>The <code>ingestDocument()</code> method handles the actual ingestion for individual documents. It verifies that the document has the required fields, sets up metadata, chunks the content, generates embeddings and saves them to Qdrant. It also confirms that the number of vectors is consistent with the number of chunks; if not, it sends a warning and skips that document.</p><p>The <code>createQdrantPoints()</code> method is a helper that combines our chunks, vectors and metadata into the format Qdrant expects.</p><h3 id="documents-controller-and-module">Documents Controller and Module</h3><p>Update your <code>documents.controller.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Body<span class="token punctuation">,</span> Controller<span class="token punctuation">,</span> Post <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> DocumentIngestionService<span class="token punctuation">,</span> IRawDocument <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./document-ingestion/document-ingestion.service'</span><span class="token punctuation">;</span>

@<span class="token function">Controller</span><span class="token punctuation">(</span><span class="token string">'documents'</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">DocumentsController</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> readonly ingestionService<span class="token punctuation">:</span> DocumentIngestionService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  @<span class="token function">Post</span><span class="token punctuation">(</span><span class="token string">'ingest'</span><span class="token punctuation">)</span>
  <span class="token keyword">async</span> <span class="token function">ingest</span><span class="token punctuation">(</span>@<span class="token function">Body</span><span class="token punctuation">(</span><span class="token punctuation">)</span> body<span class="token punctuation">:</span> <span class="token punctuation">{</span> docs<span class="token punctuation">:</span> IRawDocument<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>body<span class="token operator">?</span><span class="token punctuation">.</span>docs<span class="token operator">?</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> error<span class="token punctuation">:</span> <span class="token string">'No documents provided'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ingestionService<span class="token punctuation">.</span><span class="token function">ingestDocuments</span><span class="token punctuation">(</span>body<span class="token punctuation">.</span>docs<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Next, update the <code>DocumentsModule to import the EmbeddingsModule</code> and <code>QdrantModule</code>.</p><h2 id="building-the-search-service">Building the Search Service</h2><p>The search service handles user queries. Update your <code>search.service.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> EmbeddingsService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../embeddings/embeddings.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> QdrantService<span class="token punctuation">,</span> IQdrantPayload <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../qdrant/qdrant.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> Schemas <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@qdrant/js-client-rest'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">ISearchResult</span> <span class="token punctuation">{</span>
  title<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  snippet<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  url<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  category<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">;</span>
  score<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
  chunkIndex<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

@<span class="token function">Injectable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">SearchService</span> <span class="token punctuation">{</span>
  <span class="token keyword">private</span> <span class="token keyword">static</span> readonly MIN_LIMIT <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
  <span class="token keyword">private</span> <span class="token keyword">static</span> readonly MAX_LIMIT <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">;</span>
  <span class="token keyword">private</span> <span class="token keyword">static</span> readonly DEFAULT_LIMIT <span class="token operator">=</span> <span class="token number">10</span><span class="token punctuation">;</span>

  <span class="token keyword">constructor</span><span class="token punctuation">(</span>
    <span class="token keyword">private</span> readonly embeddings<span class="token punctuation">:</span> EmbeddingsService<span class="token punctuation">,</span>
    <span class="token keyword">private</span> readonly qdrant<span class="token punctuation">:</span> QdrantService<span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">mapHits</span><span class="token punctuation">(</span>hits<span class="token punctuation">:</span> Schemas\<span class="token punctuation">[</span><span class="token string">'ScoredPoint'</span>\<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">:</span> ISearchResult<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> hits
      <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>hit <span class="token operator">=&gt;</span> hit<span class="token punctuation">.</span>payload <span class="token operator">&amp;&amp;</span> hit<span class="token punctuation">.</span>score <span class="token operator">!==</span> undefined<span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>hit <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
        <span class="token keyword">const</span> payload <span class="token operator">=</span> hit<span class="token punctuation">.</span>payload <span class="token keyword">as</span> IQdrantPayload<span class="token punctuation">;</span>
        <span class="token keyword">return</span> <span class="token punctuation">{</span>
          title<span class="token punctuation">:</span> <span class="token function">String</span><span class="token punctuation">(</span>payload<span class="token operator">?</span><span class="token punctuation">.</span>title <span class="token operator">?</span><span class="token operator">?</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
          snippet<span class="token punctuation">:</span> <span class="token function">String</span><span class="token punctuation">(</span>payload<span class="token operator">?</span><span class="token punctuation">.</span>text <span class="token operator">?</span><span class="token operator">?</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
          url<span class="token punctuation">:</span> <span class="token function">String</span><span class="token punctuation">(</span>payload<span class="token operator">?</span><span class="token punctuation">.</span>url <span class="token operator">?</span><span class="token operator">?</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
          category<span class="token punctuation">:</span> <span class="token function">String</span><span class="token punctuation">(</span>payload<span class="token operator">?</span><span class="token punctuation">.</span>category <span class="token operator">?</span><span class="token operator">?</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
          score<span class="token punctuation">:</span> hit<span class="token punctuation">.</span>score <span class="token operator">?</span><span class="token operator">?</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token comment">//similarity score</span>
          chunkIndex<span class="token punctuation">:</span> <span class="token function">Number</span><span class="token punctuation">(</span>payload<span class="token operator">?</span><span class="token punctuation">.</span>chunkIndex <span class="token operator">?</span><span class="token operator">?</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">validateAndNormalizeQuery</span><span class="token punctuation">(</span>query<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> trimmed <span class="token operator">=</span> query<span class="token operator">?</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> trimmed <span class="token operator">&amp;&amp;</span> trimmed<span class="token punctuation">.</span>length <span class="token operator">&gt;</span> <span class="token number">0</span> <span class="token operator">?</span> trimmed <span class="token punctuation">:</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">clampLimit</span><span class="token punctuation">(</span>limit<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">number</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> Math<span class="token punctuation">.</span><span class="token function">max</span><span class="token punctuation">(</span>SearchService<span class="token punctuation">.</span>MIN_LIMIT<span class="token punctuation">,</span> Math<span class="token punctuation">.</span><span class="token function">min</span><span class="token punctuation">(</span>SearchService<span class="token punctuation">.</span>MAX_LIMIT<span class="token punctuation">,</span> limit<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">createCategoryFilter</span><span class="token punctuation">(</span>category<span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token operator">|</span> undefined<span class="token punctuation">)</span><span class="token punctuation">:</span> Schemas\<span class="token punctuation">[</span><span class="token string">'SearchRequest'</span>\<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'filter'</span><span class="token punctuation">]</span> <span class="token operator">|</span> undefined <span class="token punctuation">{</span>
    <span class="token keyword">const</span> trimmed <span class="token operator">=</span> category<span class="token operator">?</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>trimmed<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> undefined<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> <span class="token punctuation">{</span>
      must<span class="token punctuation">:</span> <span class="token punctuation">[</span>
        <span class="token punctuation">{</span>
          key<span class="token punctuation">:</span> <span class="token string">'category'</span><span class="token punctuation">,</span>
          match<span class="token punctuation">:</span> <span class="token punctuation">{</span> value<span class="token punctuation">:</span> trimmed <span class="token punctuation">}</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
      <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token keyword">async</span> <span class="token function">performSearch</span><span class="token punctuation">(</span>
    query<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
    limit<span class="token punctuation">:</span> <span class="token keyword">number</span><span class="token punctuation">,</span>
    filter<span class="token operator">?</span><span class="token punctuation">:</span> Schemas\<span class="token punctuation">[</span><span class="token string">'SearchRequest'</span>\<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'filter'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>ISearchResult<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
      <span class="token keyword">const</span> vector <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>embeddings<span class="token punctuation">.</span><span class="token function">embed</span><span class="token punctuation">(</span>query<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">const</span> hits <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>qdrant<span class="token punctuation">.</span><span class="token function">search</span><span class="token punctuation">(</span>vector<span class="token punctuation">,</span> limit<span class="token punctuation">,</span> filter<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">mapHits</span><span class="token punctuation">(</span>hits<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Error performing search:'</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>

  
  <span class="token keyword">async</span> <span class="token function">search</span><span class="token punctuation">(</span>query<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span> limit <span class="token operator">=</span> SearchService<span class="token punctuation">.</span>DEFAULT_LIMIT<span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>ISearchResult<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> normalizedQuery <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">validateAndNormalizeQuery</span><span class="token punctuation">(</span>query<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>normalizedQuery<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">performSearch</span><span class="token punctuation">(</span>normalizedQuery<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">clampLimit</span><span class="token punctuation">(</span>limit<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">async</span> <span class="token function">searchWithCategory</span><span class="token punctuation">(</span>
    query<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
    category<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token operator">|</span> <span class="token keyword">null</span><span class="token punctuation">,</span>
    limit <span class="token operator">=</span> SearchService<span class="token punctuation">.</span>DEFAULT_LIMIT<span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">:</span> Promise<span class="token operator">&lt;</span>ISearchResult<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> normalizedQuery <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">validateAndNormalizeQuery</span><span class="token punctuation">(</span>query<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>normalizedQuery<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">const</span> filter <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">createCategoryFilter</span><span class="token punctuation">(</span>category<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">performSearch</span><span class="token punctuation">(</span>normalizedQuery<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">clampLimit</span><span class="token punctuation">(</span>limit<span class="token punctuation">)</span><span class="token punctuation">,</span> filter<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>The <code>mapHits()</code> method converts Qdrant&rsquo;s raw response into a user-friendly format. The <code>validateAndNormalizeQuery()</code> method verifies we have an actual query string, <code>clampLimit()</code> keeps result counts within safe limits and <code>createCategoryFilter()</code> builds the Qdrant filter object when users filter by category.</p><p>The <code>performSearch()</code> method embeds the user query, searches Qdrant and maps the results.</p><p>The <code>search()</code> method uses only pure semantic search with no filters, while <code>searchWithCategory()</code> adds category filtering for more specific searches.</p><h2 id="search-controller">Search Controller</h2><p>Our search controller exposes two endpoints. The <code>/search</code> endpoint provides pure semantic search, while <code>/search/hybrid</code> adds category filtering. Update your <code>search.controller.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Controller<span class="token punctuation">,</span> Get<span class="token punctuation">,</span> Query <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> SearchService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./search.service'</span><span class="token punctuation">;</span>

@<span class="token function">Controller</span><span class="token punctuation">(</span><span class="token string">'search'</span><span class="token punctuation">)</span>
<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">SearchController</span> <span class="token punctuation">{</span>
  <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> readonly searchService<span class="token punctuation">:</span> SearchService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>

  <span class="token keyword">private</span> <span class="token function">parseLimit</span><span class="token punctuation">(</span>limit<span class="token punctuation">:</span> <span class="token keyword">string</span> <span class="token operator">|</span> undefined<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">number</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> parsed <span class="token operator">=</span> <span class="token function">Number</span><span class="token punctuation">(</span>limit<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token function">isNaN</span><span class="token punctuation">(</span>parsed<span class="token punctuation">)</span> <span class="token operator">||</span> parsed <span class="token operator">&lt;=</span> <span class="token number">0</span> <span class="token operator">?</span> <span class="token number">10</span> <span class="token punctuation">:</span> parsed<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  @<span class="token function">Get</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token keyword">async</span> <span class="token function">search</span><span class="token punctuation">(</span>
    @<span class="token function">Query</span><span class="token punctuation">(</span><span class="token string">'q'</span><span class="token punctuation">)</span> q<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
    @<span class="token function">Query</span><span class="token punctuation">(</span><span class="token string">'limit'</span><span class="token punctuation">)</span> limit<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>q<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> <span class="token punctuation">{</span> error<span class="token punctuation">:</span> <span class="token string">'Query parameter "q" is required'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>searchService<span class="token punctuation">.</span><span class="token function">search</span><span class="token punctuation">(</span>q<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">parseLimit</span><span class="token punctuation">(</span>limit<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  @<span class="token function">Get</span><span class="token punctuation">(</span><span class="token string">'hybrid'</span><span class="token punctuation">)</span>
  <span class="token keyword">async</span> <span class="token function">hybrid</span><span class="token punctuation">(</span>
    @<span class="token function">Query</span><span class="token punctuation">(</span><span class="token string">'q'</span><span class="token punctuation">)</span> q<span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
    @<span class="token function">Query</span><span class="token punctuation">(</span><span class="token string">'category'</span><span class="token punctuation">)</span> category<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
    @<span class="token function">Query</span><span class="token punctuation">(</span><span class="token string">'limit'</span><span class="token punctuation">)</span> limit<span class="token operator">?</span><span class="token punctuation">:</span> <span class="token keyword">string</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>q<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">return</span> <span class="token punctuation">{</span> error<span class="token punctuation">:</span> <span class="token string">'Query parameter "q" is required'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>searchService<span class="token punctuation">.</span><span class="token function">searchWithCategory</span><span class="token punctuation">(</span>q<span class="token punctuation">,</span> category<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">parseLimit</span><span class="token punctuation">(</span>limit<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Finally, update the <code>SearchModule</code> to import the <code>EmbeddingsModule</code> and <code>QdrantModule</code>.</p><h2 id="application-warmup">Application Warmup</h2><p>We don&rsquo;t want the first user request to hang while our ML model loads, so we&rsquo;ll add a warmup phase during application startup.</p><p>Update your <code>main.ts</code> file with the following:</p><pre class=" language-ts"><code class="prism  language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> NestFactory <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/core'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> AppModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.module'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> EmbeddingsService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./embeddings/embeddings.service'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> QdrantService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./qdrant/qdrant.service'</span><span class="token punctuation">;</span>

<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">bootstrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> app <span class="token operator">=</span> <span class="token keyword">await</span> NestFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>AppModule<span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">try</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Starting services warmup...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> embeddings <span class="token operator">=</span> app<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>EmbeddingsService<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> qdrant <span class="token operator">=</span> app<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>QdrantService<span class="token punctuation">)</span><span class="token punctuation">;</span>
    
    <span class="token keyword">await</span> Promise<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token punctuation">[</span>
      embeddings<span class="token punctuation">.</span><span class="token function">warmup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
      qdrant<span class="token punctuation">.</span><span class="token function">setupCollection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'✓ Services ready.'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">err</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Warmup failed'</span><span class="token punctuation">,</span> err<span class="token punctuation">)</span><span class="token punctuation">;</span>
    process<span class="token punctuation">.</span><span class="token function">exit</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">await</span> app<span class="token punctuation">.</span><span class="token function">listen</span><span class="token punctuation">(</span><span class="token number">3000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">bootstrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre><p>This loads the ML model and sets up the database collection before accepting requests.</p><h2 id="testing-the-api">Testing the API</h2><p>Run the following in your terminal to start your server:</p><pre class=" language-shell"><code class="prism  language-shell">npm run start:dev
</code></pre><p>You should see the warmup logs followed by the server start message.</p><h3 id="ingesting-documents">Ingesting Documents</h3><p>Let&rsquo;s add some test documents:</p><pre class=" language-shell"><code class="prism  language-shell">curl -X POST http://localhost:3000/documents/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "docs": [
      {
        "title": "API Authentication",
        "category": "Security",
        "url": "/docs/auth",
        "content": "To access the API, you must use a Bearer token in the header. Tokens expire after 1 hour."
      },
      {
        "title": "Rate Limiting",
        "category": "Performance",
        "url": "/docs/rate-limits",
        "content": "We limit requests to 100 per minute per IP address. Exceeding this returns a 429 Too Many Requests error."
      }
    ]
  }'
</code></pre><p>Your response should be:</p><pre class=" language-shell"><code class="prism  language-shell">{
  "status": "ok",
  "documents": 2,
  "totalChunks": 2,
  "skipped": 0
}
</code></pre><h3 id="searching-documents">Searching Documents</h3><p>Let&rsquo;s try a semantic search. Note that we&rsquo;re not using the exact words &ldquo;Bearer&rdquo; or &ldquo;header&rdquo;:</p><pre class=" language-shell"><code class="prism  language-shell">curl "http://localhost:3000/search?q=how%20do%20I%20log%20in%20to%20the%20api"
</code></pre><p>The system understands that &ldquo;log in&rdquo; is semantically related to &ldquo;authentication&rdquo; and &ldquo;Bearer token,&rdquo; so it returns the Authentication document.</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/sample-response-for-semantic-search.png?sfvrsn=d20d142e_2" title="Sample response for semantic search" alt="Sample response for semantic search" /></p><h3 id="testing-hybrid-search">Testing Hybrid Search</h3><p>Let&rsquo;s try searching with a category filter:</p><pre class=" language-shell"><code class="prism  language-shell">curl "http://localhost:3000/search/hybrid?q=api%20limits&amp;category=Performance"
</code></pre><p>Your response should be:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/sample-response-for-category-filter.png?sfvrsn=29ba3928_2" title="Sample response for category filter" alt="Sample response for category filter" /></p><p>This searches for content semantically related to &ldquo;api limits&rdquo; but only returns results in the &ldquo;Performance&rdquo; category.</p><h2 id="conclusion">Conclusion</h2><p>In this article, we learned how to run a vector database locally with Docker, generate embeddings without external APIs, chunk documents with overlapping windows and combine vector similarity with metadata filtering.</p><p>Possible next steps include swapping Xenova for OpenAI if we need larger models, or moving Qdrant to a cloud cluster for millions of vectors, while preserving the existing NestJS logic.</p><aside><hr data-sf-ec-immutable="" /><div class="row"><div class="col-4 u-normal-full u-small-mb0"><h4 class="u-fs20 u-fw5 u-lh125 u-mb0">What We&rsquo;ve Learned Designing Dev Tools in the Age of AI</h4></div><div class="col-8"><p class="u-fs16 u-mb0">AI didn&rsquo;t just change how we write code. It changed how we build products. <a target="_blank" href="https://www.telerik.com/blogs/what-weve-learned-designing-dev-tools-ai">Hear four lessons from real tech professionals building dev tools.</a></p></div></div></aside><img src="https://feeds.telerik.com/link/10827/17336853.gif" height="1" width="1"/>]]></content>
  </entry>
  <entry>
    <id>urn:uuid:bb2fa99e-0c29-407f-9eb9-4d2b43a9632a</id>
    <title type="text">Creating More Realistic Tests with In-Memory Databases in ASP.NET Core</title>
    <summary type="text">Testing ASP.NET Core APIs with in-memory SQLite and JustMock enables validation of real-world scenarios like pagination, keys and rules without a real database.</summary>
    <published>2026-05-06T17:20:57Z</published>
    <updated>2026-06-06T22:09:25Z</updated>
    <author>
      <name>Assis Zang </name>
    </author>
    <link rel="alternate" href="https://feeds.telerik.com/link/10827/17335641/creating-more-realistic-tests-memory-databases-aspnet-core"/>
    <content type="text"><![CDATA[<p><span class="featured">Testing ASP.NET Core APIs with in-memory SQLite and JustMock enables validation of real-world scenarios like pagination, keys and rules without a real database.</span></p><p>Testing a web API involves much more than simply checking whether methods return or send data correctly. It requires validating real-world behaviors like pagination, unique keys, data persistence and other database rules. Anticipating and reproducing these scenarios can be challenging, especially when they depend on specific query behaviors or stored data.</p><p>To address this, the .NET ecosystem provides powerful tools for unit testing. In this post, we explore how in-memory SQLite serves as an effective solution for testing ASP.NET Core APIs, enabling developers to simulate common scenarios without deploying a real database. Additionally, we demonstrate how tools like Progress Telerik JustMock can streamline test implementation and help validate business rules efficiently.</p><h2 id="the-importance-of-testing-in-real-world-environments">The Importance of Testing in Real-World Environments</h2><p>A common example of functionality in web APIs is paginated search, which at first glance is quite simple. We receive parameters such as <code>page</code> and <code>pageSize</code>, apply a Skip and a Take, return the data and move on. In a controlled scenario, with few records, it&rsquo;s difficult for something to go wrong.</p><p>The problem begins when this code reaches production or even test environments, without having been validated with a real database. Consider a common scenario: in a paginated search, sorting is essential for it to function correctly. Without an explicit <code>OrderBy</code>, the database does not guarantee the order of the returned records.</p><p>In in-memory lists, the result usually appears stable, which completely masks the problem. Without tests running this query on a relational database, the error goes unnoticed, resulting in inconsistent pagination with duplicate records between pages, items &ldquo;disappearing&rdquo; when navigating between pages or different results for the same request. This type of bug is especially difficult to reproduce locally when not using a real database or even a simulation of one.</p><p>This is where the in-memory database plays a very important role. It allows the query to be executed exactly as it will be executed in production, revealing flaws that only appear when pagination, sorting and the database work together.</p><h2 id="understanding-in-memory-sqlite">Understanding In-Memory SQLite</h2><p>In-memory SQLite is a way to use the SQLite database entirely in memory. Instead of saving data to a <code>.db</code> file, SQLite creates the database only while the application or connection is active. When the connection is closed, all data is discarded.</p><p>In-memory SQLite is widely used in automated testing, especially in ASP.NET Core applications in conjunction with Entity Framework Core, as it has advanced features such as simulating a real database and executing real SQL (constraints, indexes, unique keys, FK and others).</p><h2 id="testing-with-sqlite-in-memory">Testing with SQLite in Memory</h2><p>Next, we&rsquo;ll look at some examples of unit tests in common web API scenarios where in-memory SQLite excels. But first, let&rsquo;s create the basic application and then add the tests.</p><p>You can access all the code discussed in this post in this GitHub repository: <a target="_blank" href="https://github.com/zangassis/memo-order">Memo Order source code</a>.</p><p>To create the application you can use the command below:</p><pre class=" language-bash"><code class="prism  language-bash">dotnet new web -o MemoOrder
</code></pre><p>To add the NuGet packages you can use the following commands:</p><pre class=" language-bash"><code class="prism  language-bash">dotnet add package Microsoft.EntityFrameworkCore --version 10.0.1
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.1
</code></pre><p>Then, open the application and create the following classes:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">namespace</span> MemoOrder<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Order</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">int</span> Id <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span> Number <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">string</span><span class="token punctuation">.</span>Empty<span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">decimal</span> TotalAmount <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> DateTime CreatedAt <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">bool</span> IsDeleted <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">namespace</span> MemoOrder<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">OrderSummaryDto</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">string</span> Number <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">decimal</span> Total <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">using</span> Microsoft<span class="token punctuation">.</span>EntityFrameworkCore<span class="token punctuation">;</span>

<span class="token keyword">namespace</span> MemoOrder<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">AppDbContext</span> <span class="token punctuation">:</span> DbContext
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token function">AppDbContext</span><span class="token punctuation">(</span>DbContextOptions<span class="token operator">&lt;</span>AppDbContext<span class="token operator">&gt;</span> options<span class="token punctuation">)</span>
        <span class="token punctuation">:</span> <span class="token keyword">base</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>

    <span class="token keyword">public</span> DbSet<span class="token operator">&lt;</span>Order<span class="token operator">&gt;</span> Orders <span class="token operator">=</span><span class="token operator">&gt;</span> <span class="token generic-method function">Set<span class="token punctuation">&lt;</span>Order<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token keyword">void</span> <span class="token function">OnModelCreating</span><span class="token punctuation">(</span>ModelBuilder modelBuilder<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        modelBuilder<span class="token punctuation">.</span><span class="token generic-method function">Entity<span class="token punctuation">&lt;</span>Order<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>entity <span class="token operator">=</span><span class="token operator">&gt;</span>
        <span class="token punctuation">{</span>
            entity<span class="token punctuation">.</span><span class="token function">HasKey</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>Id<span class="token punctuation">)</span><span class="token punctuation">;</span>
            entity<span class="token punctuation">.</span><span class="token function">Property</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>Number<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">IsRequired</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            entity<span class="token punctuation">.</span><span class="token function">HasIndex</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>Number<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">IsUnique</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
entity<span class="token punctuation">.</span><span class="token function">HasQueryFilter</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> <span class="token operator">!</span>o<span class="token punctuation">.</span>IsDeleted<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">using</span> Microsoft<span class="token punctuation">.</span>EntityFrameworkCore<span class="token punctuation">;</span>

<span class="token keyword">namespace</span> MemoOrder<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">OrderRepository</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> AppDbContext _context<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">OrderRepository</span><span class="token punctuation">(</span>AppDbContext context<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _context <span class="token operator">=</span> context<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">AddAsync</span><span class="token punctuation">(</span>Order order<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">await</span> _context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token keyword">async</span> Task<span class="token operator">&lt;</span>List<span class="token operator">&lt;</span>Order<span class="token operator">&gt;</span><span class="token operator">&gt;</span> <span class="token function">Pagination</span><span class="token punctuation">(</span>AppDbContext context<span class="token punctuation">,</span> <span class="token keyword">int</span> skip<span class="token punctuation">,</span> <span class="token keyword">int</span> take<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders
                    <span class="token punctuation">.</span><span class="token function">OrderBy</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>Id<span class="token punctuation">)</span>
                    <span class="token punctuation">.</span><span class="token function">Skip</span><span class="token punctuation">(</span>skip<span class="token punctuation">)</span>
                    <span class="token punctuation">.</span><span class="token function">Take</span><span class="token punctuation">(</span>take<span class="token punctuation">)</span>
                    <span class="token punctuation">.</span><span class="token function">ToListAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Now let&rsquo;s create a special factory class that will be used in the tests to create the in-memory database and keep the connection open while the tests are running. Create the following class in the application:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token keyword">namespace</span> MemoOrder<span class="token punctuation">;</span>

<span class="token keyword">using</span> Microsoft<span class="token punctuation">.</span>Data<span class="token punctuation">.</span>Sqlite<span class="token punctuation">;</span>
<span class="token keyword">using</span> Microsoft<span class="token punctuation">.</span>EntityFrameworkCore<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">SqliteInMemoryFactory</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> AppDbContext <span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">var</span> connection <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SqliteConnection</span><span class="token punctuation">(</span><span class="token string">"DataSource=:memory:"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        connection<span class="token punctuation">.</span><span class="token function">Open</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">var</span> options <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">DbContextOptionsBuilder</span><span class="token operator">&lt;</span>AppDbContext<span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">UseSqlite</span><span class="token punctuation">(</span>connection<span class="token punctuation">)</span>
            <span class="token punctuation">.</span>Options<span class="token punctuation">;</span>

        <span class="token keyword">var</span> context <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AppDbContext</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span>
        context<span class="token punctuation">.</span>Database<span class="token punctuation">.</span><span class="token function">EnsureCreated</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">return</span> context<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><p>Note that to tell SQLite to use an in-memory database, we use <code>DataSource=:memory:</code> in the connection string.</p><h3 id="creating-the-test-project">Creating the Test Project</h3><p>All the code needed to implement the unit tests is ready, so let&rsquo;s create the test project and download the NuGet packages for it. To do this, you can use the following commands, simply run them in a terminal in the application&rsquo;s root directory:</p><pre class=" language-bash"><code class="prism  language-bash">dotnet new xunit -n MemoOrder.Tests
<span class="token function">cd</span> MemoOrder.Tests
dotnet add package Microsoft.NET.Test.Sdk --version 18.0.1
dotnet add package xunit --version 2.9.3
dotnet add package xunit.runner.visualstudio --version 3.1.5
dotnet add package coverlet.collector --version 6.0.4
dotnet add reference <span class="token punctuation">..</span>/MemoOrder/MemoOrder.csproj
</code></pre><p>Next, inside the project MemoOrder.Tests, create a new class called OrderRepositoryTests and let&rsquo;s add tests to it.</p><h3 id="testing-persistence">Testing Persistence</h3><p>The first test will be used to validate persistence&mdash;that is, to insert data into the database (which in this case will be the in-memory database). To do this, simply add the following test method to the <code>OrderRepositoryTests</code> class:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">AddAsync_ShouldPersistOrder</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Arrange</span>
    <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">var</span> repository <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OrderRepository</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">var</span> order <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Order</span>
    <span class="token punctuation">{</span>
        Number <span class="token operator">=</span> <span class="token string">"ORD-2026-0001"</span><span class="token punctuation">,</span>
        TotalAmount <span class="token operator">=</span> 200m<span class="token punctuation">,</span>
        CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow
    <span class="token punctuation">}</span><span class="token punctuation">;</span>

    <span class="token comment">// Act</span>
    <span class="token keyword">await</span> repository<span class="token punctuation">.</span><span class="token function">AddAsync</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Assert</span>
    <span class="token keyword">var</span> savedOrder <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">FirstAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span><span class="token string">"ORD-2026-0001"</span><span class="token punctuation">,</span> savedOrder<span class="token punctuation">.</span>Number<span class="token punctuation">)</span><span class="token punctuation">;</span>
    Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span>200m<span class="token punctuation">,</span> savedOrder<span class="token punctuation">.</span>TotalAmount<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>Note that we use <code>var context = SqliteInMemoryFactory.Create();</code> to create and represent the database in memory. It works exactly like the EF Core context that maps database tables to class entities.</p><h3 id="testing-constraint-unique">Testing Constraint UNIQUE</h3><p>Another common mistake is allowing duplicate records to reach the production environment, which in many cases can have catastrophic impacts. With in-memory testing, we can anticipate problems like this. Simply create a test that verifies inserting a duplicate record will generate an exception. In this case, we could do the following:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">Should_Throw_When_Duplicated_Order_Number</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Order</span>
    <span class="token punctuation">{</span>
        Number <span class="token operator">=</span> <span class="token string">"ORD-001"</span><span class="token punctuation">,</span>
        TotalAmount <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">,</span>
        CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Order</span>
    <span class="token punctuation">{</span>
        Number <span class="token operator">=</span> <span class="token string">"ORD-001"</span><span class="token punctuation">,</span>
        TotalAmount <span class="token operator">=</span> <span class="token number">200</span><span class="token punctuation">,</span>
        CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">await</span> Assert<span class="token punctuation">.</span><span class="token generic-method function">ThrowsAsync<span class="token punctuation">&lt;</span>DbUpdateException<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>In this test, we created an instance of the database in memory, added an item to the table and saved it.</p><p>Then, we tried to add the same item, which generates an exception because, in the dbContext configuration, we declared the Number property of the Order entity as Unique: <code>entity.HasIndex(o =&gt; o.Number).IsUnique();</code>.</p><p>Thus, we have a real-world scenario, demonstrating that duplicate records will not reach the production environment, as the unit test is able to reproduce exactly the same result.</p><h3 id="testing-pagination">Testing Pagination</h3><p>To test pagination using an in-memory database, we could do the following:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">Should_Return_Paginated_Orders</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">var</span> repository <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OrderRepository</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> i <span class="token operator">&lt;=</span> <span class="token number">10</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Order</span>
        <span class="token punctuation">{</span>
            Number <span class="token operator">=</span> $<span class="token string">"ORD-{i}"</span><span class="token punctuation">,</span>
            TotalAmount <span class="token operator">=</span> i <span class="token operator">*</span> <span class="token number">10</span><span class="token punctuation">,</span>
            CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    List<span class="token operator">&lt;</span>Order<span class="token operator">&gt;</span> page <span class="token operator">=</span> <span class="token keyword">await</span> repository<span class="token punctuation">.</span><span class="token function">Pagination</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token number">5</span><span class="token punctuation">,</span> <span class="token number">5</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">,</span> page<span class="token punctuation">.</span>Count<span class="token punctuation">)</span><span class="token punctuation">;</span>
    Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span><span class="token string">"ORD-6"</span><span class="token punctuation">,</span> page<span class="token punctuation">.</span><span class="token function">First</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>Number<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>Note that here we create 10 items and insert them into the in-memory database. Then, we use the <code>Pagination(context, 5, 5)</code> method to perform the pagination and finally verify if the expected quantity and item are correct.</p><p>An in-memory database wouldn&rsquo;t be mandatory to test pagination. This could be done using only regular in-memory lists. However, in this way, we can reproduce the same production scenario.</p><h3 id="testing-soft-delete">Testing Soft Delete</h3><p>Soft-delete is a technique where data is not physically removed from the database, but rather marked as inactive through a flag, such as an <code>isDeleted</code> or <code>deleted_at</code> column. This allows you to keep the entire transaction history in the database.</p><p>The problem is that in some cases soft delete can leak unwanted data into reports, new endpoints or even refactored queries. To prevent this from happening, we can use the in-memory database and simulate a search with and without soft-delete:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
 <span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">SoftDeleted_Orders_Should_Not_Appear_In_Default_Queries</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
 <span class="token punctuation">{</span>
     <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

     context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">AddRange</span><span class="token punctuation">(</span>
         <span class="token keyword">new</span> <span class="token class-name">Order</span>
         <span class="token punctuation">{</span>
             Number <span class="token operator">=</span> <span class="token string">"ORD-001"</span><span class="token punctuation">,</span>
             TotalAmount <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">,</span>
             CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow<span class="token punctuation">,</span>
             IsDeleted <span class="token operator">=</span> <span class="token keyword">false</span>
         <span class="token punctuation">}</span><span class="token punctuation">,</span>
         <span class="token keyword">new</span> <span class="token class-name">Order</span>
         <span class="token punctuation">{</span>
             Number <span class="token operator">=</span> <span class="token string">"ORD-002"</span><span class="token punctuation">,</span>
             TotalAmount <span class="token operator">=</span> <span class="token number">200</span><span class="token punctuation">,</span>
             CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow<span class="token punctuation">,</span>
             IsDeleted <span class="token operator">=</span> <span class="token keyword">true</span>
         <span class="token punctuation">}</span>
     <span class="token punctuation">)</span><span class="token punctuation">;</span>

     <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

     <span class="token keyword">var</span> orders <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">ToListAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

     Assert<span class="token punctuation">.</span><span class="token function">Single</span><span class="token punctuation">(</span>orders<span class="token punctuation">)</span><span class="token punctuation">;</span>
     Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span><span class="token string">"ORD-001"</span><span class="token punctuation">,</span> orders<span class="token punctuation">.</span><span class="token function">First</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>Number<span class="token punctuation">)</span><span class="token punctuation">;</span>
 <span class="token punctuation">}</span>
</code></pre><p>In this test, the list returns only one item because the second one is marked as deleted. This happens because we declared the query filter <code>entity.HasQueryFilter(o =&gt; !o.IsDeleted);</code> when the database is created. By using an in-memory database, we can verify this mechanism works.</p><p>It&rsquo;s also possible to ignore the query filter and still verify it works:</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">Admin_Query_Should_See_SoftDeleted_Orders</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">AddRange</span><span class="token punctuation">(</span>
        <span class="token keyword">new</span> <span class="token class-name">Order</span>
        <span class="token punctuation">{</span>
            Number <span class="token operator">=</span> <span class="token string">"ORD-001"</span><span class="token punctuation">,</span>
            TotalAmount <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">,</span>
            CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow<span class="token punctuation">,</span>
            IsDeleted <span class="token operator">=</span> <span class="token keyword">false</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
        <span class="token keyword">new</span> <span class="token class-name">Order</span>
        <span class="token punctuation">{</span>
            Number <span class="token operator">=</span> <span class="token string">"ORD-002"</span><span class="token punctuation">,</span>
            TotalAmount <span class="token operator">=</span> <span class="token number">200</span><span class="token punctuation">,</span>
            CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow<span class="token punctuation">,</span>
            IsDeleted <span class="token operator">=</span> <span class="token keyword">true</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">var</span> orders <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">IgnoreQueryFilters</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToListAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> orders<span class="token punctuation">.</span>Count<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="testing-rollback">Testing Rollback</h3><p>When performing multiple operations, we run the risk of something failing during the process and the data becoming inconsistent. To keep this from happening, we can use the in-memory database to test if the rollback operation solves this problem.</p><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">Transaction_Should_Rollback_When_Error_Occurs</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">using</span> <span class="token keyword">var</span> transaction <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Database<span class="token punctuation">.</span><span class="token function">BeginTransactionAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Order</span>
    <span class="token punctuation">{</span>
        Number <span class="token operator">=</span> <span class="token string">"ORD-NMBR-1"</span><span class="token punctuation">,</span>
        TotalAmount <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">,</span>
        CreatedAt <span class="token operator">=</span> DateTime<span class="token punctuation">.</span>UtcNow
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">await</span> transaction<span class="token punctuation">.</span><span class="token function">RollbackAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">var</span> count <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders<span class="token punctuation">.</span><span class="token function">CountAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> count<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><p>In this test, we added an item to the order list, saved it and then performed a rollback. Finally, we verified that the list was empty, checking that the operation was undone.</p><h3 id="testing-query-refactoring">Testing Query Refactoring</h3><p>Refactoring queries is often necessary, whether to improve performance or simply to make the code cleaner. The problem is that this can break queries that previously worked.</p><p>To see that both the old and new queries have the same result, we can use an in-memory database as demonstrated in the example below:</p><pre class=" language-csharp"><code class="prism  language-csharp">   <span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">Refactored_Query_Should_Return_Same_Result</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">var</span> oldResult <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders
            <span class="token punctuation">.</span><span class="token function">Where</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>TotalAmount <span class="token operator">&gt;</span> <span class="token number">100</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Select</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>Id<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">ToListAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">var</span> newResult <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders
            <span class="token punctuation">.</span><span class="token function">Select</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> <span class="token keyword">new</span> <span class="token punctuation">{</span> o<span class="token punctuation">.</span>Id<span class="token punctuation">,</span> o<span class="token punctuation">.</span>TotalAmount <span class="token punctuation">}</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Where</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>TotalAmount <span class="token operator">&gt;</span> <span class="token number">100</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Select</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>Id<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">ToListAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        Assert<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span>oldResult<span class="token punctuation">,</span> newResult<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
</code></pre><p>In this example, we verify that both queries have the same result, so even after refactoring, the old logic will continue to work.</p><h3 id="query-test-with-projection-to-dto">Query Test with Projection to DTO</h3><p>When using Data Transfer Objects (DTOs) for data transport, we are subject to errors that can silently break our queries. Again, with an in-memory database, we can simulate a query using a DTO.</p><pre class=" language-csharp"><code class="prism  language-csharp">   <span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">Projection_To_Dto_Should_Work</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">using</span> <span class="token keyword">var</span> context <span class="token operator">=</span> SqliteInMemoryFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">var</span> result <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span>Orders
            <span class="token punctuation">.</span><span class="token function">Select</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> <span class="token keyword">new</span> <span class="token class-name">OrderSummaryDto</span>
            <span class="token punctuation">{</span>
                Number <span class="token operator">=</span> o<span class="token punctuation">.</span>Number<span class="token punctuation">,</span>
                Total <span class="token operator">=</span> o<span class="token punctuation">.</span>TotalAmount
            <span class="token punctuation">}</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">ToListAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        Assert<span class="token punctuation">.</span><span class="token function">NotNull</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
</code></pre><p>Note that through this test, we can be sure that our DTOs will function correctly.</p><h2 id="not-all-tests-need-a-database">Not All Tests Need a Database</h2><p>Until now, we&rsquo;ve used in-memory SQLite to validate constraints (UNIQUE), global filters (Soft Delete), real pagination, GroupBy, Sum, transactions and rollback.</p><p>Tests like these depend on database behavior, but it&rsquo;s common to encounter scenarios where the database isn&rsquo;t present, and only business rules are involved. In this case, to facilitate our work and validate our tests, we can use tools like <a target="_blank" href="https://www.telerik.com/products/mocking.aspx">JustMock</a> and simulate a production-facing environment.</p><p>Next, we&rsquo;ll look at some examples of business rule tests where we can use JustMock to easily create mocks for our tests.</p><h3 id="verifying-the-repository-will-be-called">Verifying the Repository Will Be Called</h3><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">async</span> Task <span class="token function">CreateAsync_Should_Call_Repository_AddAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Arrange</span>
    <span class="token keyword">var</span> repository <span class="token operator">=</span> Mock<span class="token punctuation">.</span><span class="token generic-method function">Create<span class="token punctuation">&lt;</span>IOrderRepository<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">var</span> order <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Order</span>
    <span class="token punctuation">{</span>
        Id <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
        Number <span class="token operator">=</span> <span class="token string">"ORD-001"</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>

    Mock<span class="token punctuation">.</span><span class="token function">Arrange</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> repository<span class="token punctuation">.</span><span class="token function">AddAsync</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">Returns</span><span class="token punctuation">(</span>Task<span class="token punctuation">.</span>CompletedTask<span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">MustBeCalled</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">var</span> service <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OrderService</span><span class="token punctuation">(</span>repository<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Act</span>
    <span class="token keyword">await</span> service<span class="token punctuation">.</span><span class="token function">CreateAsync</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Assert</span>
    Mock<span class="token punctuation">.</span><span class="token function">Assert</span><span class="token punctuation">(</span>repository<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="verifying-the-gethighvalueorders-method-returns-only-orders-above-the-minimum-value">Verifying the GetHighValueOrders Method Returns Only Orders Above the Minimum Value</h3><pre class=" language-csharp"><code class="prism  language-csharp"><span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">GetHighValueOrders_Should_Return_Only_Orders_Above_MinValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Arrange</span>
    <span class="token keyword">var</span> repository <span class="token operator">=</span> Mock<span class="token punctuation">.</span><span class="token generic-method function">Create<span class="token punctuation">&lt;</span>IOrderRepository<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">var</span> orders <span class="token operator">=</span> <span class="token function">FakeOrders</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">Where</span><span class="token punctuation">(</span>o <span class="token operator">=</span><span class="token operator">&gt;</span> o<span class="token punctuation">.</span>TotalAmount <span class="token operator">&gt;=</span> <span class="token number">150</span> <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>o<span class="token punctuation">.</span>IsDeleted<span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">ToList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Mock<span class="token punctuation">.</span><span class="token function">Arrange</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> repository<span class="token punctuation">.</span><span class="token function">GetHighValueOrders</span><span class="token punctuation">(</span><span class="token number">150</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">Returns</span><span class="token punctuation">(</span>orders<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">var</span> service <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OrderService</span><span class="token punctuation">(</span>repository<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Act</span>
    <span class="token keyword">var</span> result <span class="token operator">=</span> service<span class="token punctuation">.</span><span class="token function">GetHighValueOrders</span><span class="token punctuation">(</span><span class="token number">150</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Assert</span>
    Assert<span class="token punctuation">.</span><span class="token function">AreEqual</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> result<span class="token punctuation">.</span>Count<span class="token punctuation">)</span><span class="token punctuation">;</span>
    Assert<span class="token punctuation">.</span><span class="token function">IsTrue</span><span class="token punctuation">(</span>result<span class="token punctuation">.</span><span class="token function">First</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>TotalAmount <span class="token operator">&gt;=</span> <span class="token number">150</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h3 id="verifying-the-service-forwards-exactly-the-filter-received">Verifying the Service Forwards Exactly the Filter Received</h3><pre class=" language-csharp"><code class="prism  language-csharp">       <span class="token punctuation">[</span>Fact<span class="token punctuation">]</span>
        <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">GetHighValueOrders_Should_Call_Repository_With_Same_MinValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// Arrange</span>
            <span class="token keyword">var</span> repository <span class="token operator">=</span> Mock<span class="token punctuation">.</span><span class="token generic-method function">Create<span class="token punctuation">&lt;</span>IOrderRepository<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

            Mock<span class="token punctuation">.</span><span class="token function">Arrange</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">&gt;</span> repository<span class="token punctuation">.</span><span class="token function">GetHighValueOrders</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
                <span class="token punctuation">.</span><span class="token function">Returns</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">List</span><span class="token operator">&lt;</span>Order<span class="token operator">&gt;</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
                <span class="token punctuation">.</span><span class="token function">MustBeCalled</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token keyword">var</span> service <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OrderService</span><span class="token punctuation">(</span>repository<span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token comment">// Act</span>
            service<span class="token punctuation">.</span><span class="token function">GetHighValueOrders</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token comment">// Assert</span>
            Mock<span class="token punctuation">.</span><span class="token function">Assert</span><span class="token punctuation">(</span>repository<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
</code></pre><p>If you run all the tests, you can verify that they all passed:</p><p><img src="https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2026/2026-04/all-tests-passed.png?sfvrsn=60e45a83_2" title="all tests passed" alt="All tests passed" /></p><h2 id="conclusion">Conclusion</h2><p>Testing web applications is common in software development, but these tests don&rsquo;t always guarantee that errors won&rsquo;t occur in the production environment. Using in-memory databases, such as in-memory SQLite, allows you to reproduce scenarios closer to reality.</p><p>In this post, we saw how this approach helps identify common errors, such as persistence, pagination and projection issues. Furthermore, we explored how JustMock can help save time when validating business rules. I hope the content covered here helps you further improve your applications, making them more reliable through even more realistic testing.</p><hr /><p><a href="https://www.telerik.com/try/justmock" target="_blank">Try JustMock</a> free for 30 days, or <a href="https://www.telerik.com/try/devcraft-ultimate" target="_blank">try out the whole Telerik DevCraft</a> suite for access to the ASP.NET Core component library and lots more (also a free 30-day trial).</p><img src="https://feeds.telerik.com/link/10827/17335641.gif" height="1" width="1"/>]]></content>
  </entry>
</feed>
