typescript
53 lines · 8 steps
Per-tenant database connections in NestJS
A request-scoped provider resolves the tenant from the subdomain and hands back a pooled, per-tenant TypeORM DataSource.
Explained by
highlit
1import { Injectable, Scope, Inject, NotFoundException } from '@nestjs/common';
2import { REQUEST } from '@nestjs/core';
3import { Request } from 'express';
4import { DataSource } from 'typeorm';
5
6export const TENANT_CONNECTION = 'TENANT_CONNECTION';
7
8@Injectable()
9export class TenantConnectionFactory {
10 private readonly pool = new Map<string, DataSource>();
11
12 async resolve(subdomain: string): Promise<DataSource> {
13 const existing = this.pool.get(subdomain);
14 if (existing?.isInitialized) {
15 return existing;
16 }
17
18 const tenant = await this.tenants.findBySlug(subdomain);
19 if (!tenant) {
20 throw new NotFoundException(`Unknown tenant '${subdomain}'`);
21 }
22
23 const dataSource = new DataSource({
24 type: 'postgres',
25 url: tenant.databaseUrl,
26 schema: tenant.schema,
27 entities: [__dirname + '/../**/*.entity.{ts,js}'],
28 synchronize: false,
29 });
30
31 await dataSource.initialize();
32 this.pool.set(subdomain, dataSource);
33 return dataSource;
34 }
35
36 constructor(private readonly tenants: TenantRegistryService) {}
37}
38
39export const tenantConnectionProvider = {
40 provide: TENANT_CONNECTION,
41 scope: Scope.REQUEST,
42 useFactory: (req: Request, factory: TenantConnectionFactory) => {
43 const host = req.headers.host ?? '';
44 const subdomain = host.split('.')[0]?.toLowerCase();
45
46 if (!subdomain || subdomain === 'www') {
47 throw new NotFoundException('Missing tenant subdomain');
48 }
49
50 return factory.resolve(subdomain);
51 },
52 inject: [REQUEST, TenantConnectionFactory],
53};
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Caching initialized DataSources in a Map avoids reopening a database connection on every request.
- 2A request-scoped factory provider can read per-request context like the host header to pick the right resource.
- 3Separating a singleton pool from a request-scoped resolver keeps expensive connections shared while routing stays per-request.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/per-tenant-database-connections-in-nestjs-explained-typescript-37f2/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.