Back to blog
Multi-tenancyNestJSTypeORM

NestJS TypeORM and Multi-Tenancy

A practical implementation of multi-tenancy using NestJS and TypeORM, managing multiple databases for customers, with features like database migrations and connection handling. Future improvements could include real-time database verification and a caching layer for better scalability.

Henrique Weiand

Henrique Weiand

6 min read
NestJS TypeORM and Multi-Tenancy
On this page

Not so different from any other ordinary type of project, multi-tenancy is a software architecture that has been in place for quite a while, and as with any other approach, there are pros and cons. In this article, I plan to walk you through a simple solution I implemented to tackle basic architecture inside the universe of the NestJS framework with TypeORM.

Context

I've been studying and trying new approaches whenever something pops up and I wonder how to do that technically, or when I found something cool! I've written a few articles with this idea in mind, and this one is no different! I was talking to my brother the other day and he is planning to migrate a project that runs with PHP to node and he asked me if I knew an example of multi-tenancy with NestJS. Despite being in the technology for a while, I worked on something with C++ in the past but not with Node or NestJS. In the end, it wasn't a problem, I took it as my new study subject and tried it!

The final code is available on the link below on GitHub

https://github.com/henriqueweiand/nestjs-typeorm-multi-tenancy

I don’t describe the details in this article, just the overview and how the application works according to the target, which is:

  • NestJS project;
  • Multiple databases, one for each customer;
  • Use TypeORM to deal with the database connection;

Multi-tenancy

Let me start by saying that multi-tenancy has different types, some of them are:

  • A single application, single database;
  • A single application, multiple database;
  • Multiple applications, multiple databases;

types_of_multi_tenant_architecture-f.png

You can find more details about the concept online. (What is multi-tenancy (multi-tenant architecture)).

In case you want to know, this project uses a single application and multiple databases. 🎯

Developing

Before I started, I researched NestJS packages, existing libraries, public projects, and articles and I found a few interesting contents, for example:

https://dev.to/logeek/nestjs-and-typeorm-efficient-schema-level-multi-tenancy-with-auto-generated-migrations-a-dx-approach-jla

https://github.com/mguay22/nestjs-multitenancy

None of them matched exactly what I needed, which motivated me to continue and implement it, and in the end, I got some shared knowledge from all the materials and included them into my project version.

The project has two main folders that are essential for the idea.

  • /src/libs/database
  • /src/libs/tenancy

Database

Starting with the database, it is important to note that the service implements OnModuleInit, which executes a logic when the app starts and the service is instantiated. What I implemented consists of the following sequence:

  1. App starts;
  2. The app initiates a default DB connection;
  3. This instance gets the data from a default database and table tenant which holds the database information for all the customers;
  4. This connection verifies if each database from the data is created and if so, it runs the migrations over the customer's database, otherwise, it creates a new database and also runs the migrations over it.
  5. An exclusive Data source is created for each one of the databases and stored in memory;
  6. Close the default connection (this connection is different from the other databases);

All this logic is happening inside the database.service.ts in case you want to check it out.

Source: https://github.com/henriqueweiand/nestjs-typeorm-multi-tenancy/blob/main/src/libs/database/database.service.ts

import { Inject, Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { DataSource, DataSourceOptions, Repository } from 'typeorm';
import { ClsService } from 'nestjs-cls';

import { Tenant } from 'src/libs/tenancy/entities/tanant.entity';
import { TENANT_KEY } from 'src/libs/tenancy/tenancy.constants';
import { DataSourceConfig } from './datasource.config';

@Injectable()
export class DatabaseService implements OnModuleDestroy, OnModuleInit {
  private readonly logger = new Logger(DatabaseService.name);
  private tenantConnections = new Map<string, DataSource>();
  private defaultDataSource: DataSource;

  constructor(
    @Inject(DataSourceConfig.KEY)
    private readonly dataSourceConfig: ConfigType<typeof DataSourceConfig>,
    private readonly cls: ClsService,
  ) {
  }

  private async initializeDefaultConnection() {
    const defaultDataSourceOptions: DataSourceOptions = {
      type: this.dataSourceConfig.type,
      host: this.dataSourceConfig.host,
      username: this.dataSourceConfig.username,
      password: this.dataSourceConfig.password,
      database: this.dataSourceConfig.database,
      port: this.dataSourceConfig.port,
      logging: this.dataSourceConfig.logging,
      entities: [__dirname + '/../tenancy/**/*.entity{.ts,.js}'],
      migrations: [`${__dirname}/system-migrations/*{.ts,.js}`],
      migrationsRun: true,
      synchronize: false,
    };

    this.defaultDataSource = new DataSource(defaultDataSourceOptions);
    await this.defaultDataSource.initialize();
    this.logger.log('Default connection initialized');
  }

  async onModuleInit() {
    await this.initializeDefaultConnection();
    await this._createTenantConnections();
  }

  async onModuleDestroy() {
    for (const [tenantId, dataSource] of this.tenantConnections) {
      await dataSource.destroy();
      this.logger.log(`Closed connection for tenant ${tenantId}`);
    }
  }

  private _createConnectionString(tenant: Tenant): string {
    return `${tenant.connectionType}://${tenant.username}:${tenant.password}@${tenant.host}:${tenant.port}/${tenant.database}`;
  }

  private async _createTenantConnections() {
    const tenantRepository: Repository<Tenant> = this.defaultDataSource.getRepository(Tenant);
    const tenants = await tenantRepository.find();

    for (const tenant of tenants) {
      const connectionsString = this._createConnectionString(tenant);
      await this._createTenantConnection(tenant, connectionsString);
    }

    await this.defaultDataSource.destroy();
    this.logger.log('Default connection closed');
  }

  private async _createTenantConnection(
    tenant: Tenant,
    connectionString: string,
  ) {
    await this._createDatabaseIfNotExists(tenant.database);

    const dataSourceOptions: DataSourceOptions = {
      type: tenant.connectionType as any,
      url: connectionString,
      synchronize: false,
      migrationsRun: true,
      entities: [__dirname + '/../../components/**/*.entity{.ts,.js}'],
      migrations: [__dirname + '/migrations/*{.ts,.js}'],
    };

    const dataSource = new DataSource(dataSourceOptions);
    await dataSource.initialize();

    this.tenantConnections.set(tenant.id, dataSource);
    this.logger.log(`Initialized connection ${tenant.database}`);
  }

  private async _createDatabaseIfNotExists(database: string) {
    const result = await this.defaultDataSource.query(`SELECT 1 FROM pg_database WHERE datname = '${database}'`);

    if (!result.length) {
      this.logger.log(`Creating database ${database}`);
      await this.defaultDataSource.query(`CREATE DATABASE ${database}`);
    }
  }

  /**
   * Get the data source for the current tenant
   */
  getDataSource() {
    const tenantId = this.cls.get(TENANT_KEY);

    return this.tenantConnections.get(tenantId);
  }
}

Tenancy

Despite being a simple module if you take a look, it's a crucial part of the project and it put everything together!

This module uses the library nestjs-cls, which is "A continuation-local storage module compatible with NestJS' dependency injection based on AsyncLocalStorage". You also can use the node implementation with async_hooks if you want. The module gets the tenant-id from the request and adds it to the local storage which makes the info available through the request lifecycle.

Continuation-local storage allows to store state and propagate it throughout callbacks and promise chains. It allows storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages.

Source: https://github.com/henriqueweiand/nestjs-typeorm-multi-tenancy/blob/main/src/libs/tenancy/tenancy.module.ts

import { Module } from '@nestjs/common';
import { Request } from 'express';
import { ClsModule } from 'nestjs-cls';

import { TENANT_KEY } from './tenancy.constants';

@Module({
  imports: [
    ClsModule.forRoot({
      global: false,
      middleware: {
        mount: true,
        setup: (cls, req: Request) => {
          const tenantId = req.headers['tenant-id'];

          cls.set(TENANT_KEY, tenantId);
        },
      },
    }),
  ],
  exports: [ClsModule],
})
export class TenancyModule { }

Finally, with all that in place, one method inside the database.service.ts, will help us to connect the repositories later to the right database.

/**
 * Get the data source for the current tenant
 */
getDataSource() {
  const tenantId = this.cls.get(TENANT_KEY);

  return this.tenantConnections.get(tenantId);
}

it will get the tenant-id from the local storage as explained before and also the connection from the variable in memory

Connecting a repository to the database

Different from the normal way of using TypeORM, I mean, by using the forFeature method, etc, in this case, you need to do something a bit different.

https://github.com/henriqueweiand/nestjs-typeorm-multi-tenancy/blob/main/src/components/users/users.service.ts

With the DatabaseModule module added to your Module, you can import the Database service and call

this.userRepository = this.databaseService.getDataSource().getRepository(User);

This line will return an instance of the correct database according to the tenant-id from the header and the User repository to use as needed.

And that's all my friends! We did it!!!

Bonus: Generating migrations

This project is set up in a way that you can use the TypeORM generate command! After making any change in the entities, you can run yarn typeorm:generate, and it will generate the migrations for you. You won't need to run with another command, because the application will run for you once it starts because of the migrationsRun: true

In the readme file, you can find a section about how to run the application

Source: https://github.com/henriqueweiand/nestjs-typeorm-multi-tenancy/blob/main/README.md

# NestJS-Typeorm-Multi-Tenancy

Multi Tenancy base example using TypeORM and NestJS. Article about the project: https://medium.com/@henrique.weiand/nestjs-typeorm-and-multi-tenancy-a7f6176e8319

### How to run

1. Rename .env.example to .env
2. Execute the docker `docker-compose up -d`;
3. Run once the app `yarn start:dev`
4. Stop the app, and go to the tenant table inside the nestjs-typeorm-multi-tenancy database.
5. Insert the connection data as needed (it will create new database)
6. Use the id to make requests. Inform tenant-id in the headers

### Commands

If you want to generate migrations based on the entities, you can do it by running `yarn typeorm:generate`, then, when you start the application, it will run the migrations for all bases.

Conclusion

In this article, we explored a practical implementation of multi-tenancy using NestJS and TypeORM. The solution demonstrates how to handle multiple databases for different customers while maintaining a clean and organized codebase.

The key achievements of this implementation include:

  • Successfully setting up a NestJS project with multiple database support
  • Creating a system that manages database connections for each customer
  • Implementing database migrations and proper connection handling

While this implementation provides a solid foundation, there are opportunities for improvement, such as:

  • Adding real-time database verification without requiring app restart
  • Implementing a caching layer for better connection management across scaled applications

Overall, this approach offers a practical solution for implementing multi-tenancy in NestJS applications while maintaining flexibility and scalability.

Run it in your browser

Spin up the companion repository on StackBlitz β€” no local setup required.

henriqueweiand/nestjs-typeorm-multi-tenancy

Runs on StackBlitz WebContainers

Was this helpful?

Let me know if this was useful.

Get new posts in your inbox

No spam. NestJS, TypeScript, and backend architecture β€” when there is something worth reading.

Related posts