<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: phen0menon</title>
    <description>The latest articles on DEV Community by phen0menon (@phen0menon).</description>
    <link>https://dev.to/phen0menon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3902932%2Ffcc134a3-2911-4fb3-897b-f95016140e78.jpeg</url>
      <title>DEV Community: phen0menon</title>
      <link>https://dev.to/phen0menon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phen0menon"/>
    <language>en</language>
    <item>
      <title>Multi-tenant Mongoose Module for NestJS</title>
      <dc:creator>phen0menon</dc:creator>
      <pubDate>Tue, 28 Apr 2026 19:03:38 +0000</pubDate>
      <link>https://dev.to/phen0menon/multi-tenant-mongoose-module-for-nestjs-2gic</link>
      <guid>https://dev.to/phen0menon/multi-tenant-mongoose-module-for-nestjs-2gic</guid>
      <description>&lt;p&gt;Building a multi-tenant service? If you're using NestJS and MongoDB (Mongoose), you've probably hit a wall when trying to isolate customer data.&lt;/p&gt;

&lt;p&gt;The common approach - database-per-tenant - often leads to messy code where you have to manually pass tenant IDs around or dynamically create connections on every single request. It gets even worse when you add background jobs into the mix.&lt;/p&gt;

&lt;p&gt;I got tired of the boilerplate and edge cases, so I built &lt;a href="https://github.com/phen0menon/nestjs-mongoose-tenancy" rel="noopener noreferrer"&gt;@phen0menon/nestjs-mongoose-tenancy&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The main idea
&lt;/h3&gt;

&lt;p&gt;The goal was to make it feel exactly like the official @nestjs/mongoose package, but tenant-aware.&lt;/p&gt;

&lt;p&gt;Instead of doing manual lookups in your business logic, the library handles it at the Dependency Injection layer. You just define a resolver in your &lt;code&gt;app.module.ts&lt;/code&gt; (like grabbing an &lt;code&gt;x-tenant-id&lt;/code&gt; header), and the DI container gives you the correct model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;// It just gives you the model connected to the right DB 👇&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InjectTenantModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProductDocument&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lean&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a request comes in without a tenant ID, or with an unknown one, it gracefully falls back to a mandatory common database (which is also great for storing global app settings or user accounts).&lt;/p&gt;

&lt;h3&gt;
  
  
  Background jobs?
&lt;/h3&gt;

&lt;p&gt;This was my biggest headache with other approaches. Workers (like BullMQ or cron jobs) don't have an HTTP context, so request-scoped injection breaks.&lt;/p&gt;

&lt;p&gt;To fix this, the library exposes an &lt;code&gt;@InjectTenantModelMap()&lt;/code&gt; decorator. It gives you a &lt;code&gt;ReadonlyMap&lt;/code&gt; of all your initialized tenant models. When your worker picks up a job, you just grab the specific model you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductJobHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InjectTenantModelMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;productModels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProductDocument&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productModels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tenant disconnected or missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countDocuments&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dealing with Startup Failures
&lt;/h3&gt;

&lt;p&gt;In a multi-tenant app, if one customer's database goes down, you really don't want your entire NestJS app to crash on startup.&lt;/p&gt;

&lt;p&gt;By default, the library is fail-fast. But I added a degraded startup mode. If a tenant DB is unreachable when the app boots, it skips it and starts the app anyway. Any HTTP requests routed to that specific tenant will throw a clean &lt;code&gt;TenantConnectionUnavailableException&lt;/code&gt; until the connection recovers.&lt;/p&gt;

&lt;h3&gt;
  
  
  A few other things it does
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic discovery: You don't have to hardcode tenant URIs. You can load your tenants from your common DB asynchronously during the app bootstrap phase.&lt;/li&gt;
&lt;li&gt;Zero runtime overhead: Because models are resolved at the DI level, tenant switching is blazing fast.&lt;/li&gt;
&lt;li&gt;Strict typing: It's written in TS and plays nice with strict mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've tested it with NestJS 9-11 and Mongoose 6-9.&lt;/p&gt;

&lt;p&gt;If you're building something similar and want to skip writing all this plumbing yourself, you can check it out here:&lt;/p&gt;

&lt;p&gt;🔗 GitHub: &lt;a href="https://github.com/phen0menon/nestjs-mongoose-tenancy" rel="noopener noreferrer"&gt;phen0menon/nestjs-mongoose-tenancy&lt;/a&gt;&lt;br&gt;
📦 NPM: &lt;a href="https://www.npmjs.com/package/@phen0menon/nestjs-mongoose-tenancy/access" rel="noopener noreferrer"&gt;@phen0menon/nestjs-mongoose-tenancy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are a few runnable examples in the repo (including one using mongodb-memory-server so you can actually test the connections locally). Let me know if you run into any issues or have ideas for improvements ✌️&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>mongodb</category>
      <category>mongoose</category>
      <category>tenancy</category>
    </item>
  </channel>
</rss>
