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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Caching initialized DataSources in a Map avoids reopening a database connection on every request.
  2. 2A request-scoped factory provider can read per-request context like the host header to pick the right resource.
  3. 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.

Per-tenant database connections in NestJS — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code